Skip to main content

Command Palette

Search for a command to run...

Cron vs Queue vs Event: Choosing the Right Trigger

How time, work, and causality shape execution—and how to choose without creating debt

Updated
6 min read
Cron vs Queue vs Event: Choosing the Right Trigger

Most systems don’t fail because they picked the wrong database or framework. They fail because they picked the wrong trigger.

Something runs too early. Or too late. Or too often. Or only when a user happens to click a button. The code is correct, the infrastructure is healthy—and the behavior is still wrong.

This article is about learning to choose how work starts. Not how it’s written, not how it’s optimized, but what causes it to run in the first place. The frameworks and tools code examples used in this article are derived from my real production implementation.

Cron, queues, and events are not interchangeable. They encode different assumptions about time, causality, and responsibility. Understanding those assumptions is how you avoid architectural debt that only becomes visible years later.


Three Triggers, Three Worldviews

At a high level, these mechanisms answer different questions:

  • Cron answers: “Is it time?”

  • Queue answers: “Is there work waiting?”

  • Event answers: “Did something happen?”

They may all execute code, but they live in different mental models.


Time-Triggered Execution (Cron)

Cron is time-driven. It does not care whether anything changed. It cares only that the clock says “now.”

In a Yii/HumHub setup, this often looks like:

* * * * * php yii cron/run

Inside the application:

class CleanupJob extends \humhub\components\CronJob
{
    public function run()
    {
        Post::deleteAll(['<', 'created_at', strtotime('-30 days')]);
    }

    public function getSchedule()
    {
        return self::SCHEDULE_DAILY;
    }
}

Cron is ideal when:

  • Work must happen even if nothing else happens

  • You are reconciling or enforcing invariants

  • You are comfortable with best-effort timing

  • Skipped runs are acceptable or recoverable

Cron is indifferent. It runs whether the system is busy or idle, healthy or degraded.

That indifference is its power—and its danger.


Work-Triggered Execution (Queues)

Queues are state-driven. They execute because work exists, not because time passed.

In Yii:

Yii::$app->queue->push(new SendNotificationJob([
    'userId' => $userId,
]));

And later:

php yii queue/run

Queues are ideal when:

  • Work volume is unpredictable

  • You need retries, delays, or prioritization

  • Execution should scale independently of scheduling

  • Latency matters more than exact timing

Queues care deeply about backlog. If nothing is waiting, nothing runs. If a lot is waiting, they absorb pressure instead of collapsing.

Queues answer how much and how fast. They do not answer when something should be considered in the first place.


Event-Driven Execution

Events are causality-driven. They run because something specific happened.

In application code:

Event::trigger(User::class, User::EVENT_AFTER_INSERT, new Event([
    'sender' => $user,
]));

Or conceptually:

onUserRegistered($user) {
    // react immediately
}

Events are ideal when:

  • A specific state change matters

  • The reaction must be immediate or contextual

  • You want minimal latency

  • You can tolerate missed events only rarely

Events encode meaning, not schedule. They answer why something should run.

But events are fragile when used for work that must happen regardless of user behavior.


A Simple Decision Matrix

When deciding how to trigger work, ask these questions:

1. Does this need to happen even if nobody does anything?

  • Yes → Cron

  • No → Queue or Event

2. Does this need to happen immediately when something changes?

  • Yes → Event

  • No → Cron or Queue

3. Is the amount of work unpredictable or bursty?

  • Yes → Queue

  • No → Cron or Event

4. Can this safely run twice?

  • No → Avoid cron unless idempotency or locking exists

  • Yes → Cron becomes viable

5. Is time the reason this work exists?

  • Yes → Cron

  • No → Queue or Event

Most bad designs come from answering these questions incorrectly—or not asking them at all.


Hybrid Patterns (Where Real Systems Live)

Mature systems rarely choose just one trigger. They compose them.

Pattern 1: Cron → Queue (Time Decides, Queue Executes)

This is the HumHub pattern that I’ve implemented.

* * * * * php yii cron/run
public function run()
{
    Yii::$app->queue->push(new RecalculateStatsJob());
}

Cron decides when.
The queue decides how fast.

This is ideal for periodic but heavy work.

Pattern 2: Event → Queue (Meaning Decides, Queue Executes)

Event::on(User::class, User::EVENT_AFTER_INSERT, function ($event) {
    Yii::$app->queue->push(new SendWelcomeEmailJob([
        'userId' => $event->sender->id,
    ]));
});

The event provides context.
The queue provides resilience.

This keeps user-facing actions fast while preserving intent.

Pattern 3: Event + Cron (Immediate + Reconciliation)

Events do the fast path. Cron does the safety net.

// Event-driven
onOrderPaid($order) {
    markAsProcessed($order);
}

// Cron-driven reconciliation
class OrderReconciliationJob extends CronJob {
    public function run() {
        $this->fixInconsistentOrders();
    }
}

This pattern accepts that events can be missed and uses time-based checks to restore correctness.

This is not redundancy. It is defensive architecture.


Why Misuse Creates Architectural Debt

The most expensive mistakes are subtle.

Using Cron for Event-Driven Work

Example:

“Send emails every minute and see who signed up.”

Problems:

  • Unnecessary polling

  • Delayed reactions

  • Growing query cost

  • Confusing intent

You encoded meaning (user signed up) as time (every minute). That mismatch becomes technical debt.

Using Events for Time-Based Guarantees

Example:

“Clean up expired sessions when users log in.”

What if nobody logs in?

Now cleanup depends on behavior unrelated to the task’s purpose. That’s accidental coupling.

Using Queues as a Scheduler

Example:

“Push a delayed job and hope it fires at the right time.”

Queues are not clocks. Delays drift. Retries compound. Restarts blur guarantees.

Time-based intent belongs to a scheduler, not a backlog.


The Core Insight: Triggers Encode Assumptions

Every trigger bakes in assumptions:

  • Cron assumes time is the reason work exists

  • Queues assume work volume is the problem

  • Events assume causality is the signal

If those assumptions are wrong, the system still works—just increasingly badly.

That’s why misuse is so dangerous. It doesn’t break immediately. It ages poorly.


Designing Instead of Guessing

Instead of asking:

“How should I run this code?”

Ask:

“Why should this code run at all?”

  • If the answer is “because time passed” → Cron

  • If the answer is “because work exists” → Queue

  • If the answer is “because something happened” → Event

Only after that do frameworks, tools, and syntax matter.


The Takeaway

Cron, queues, and events are not competitors. They are orthogonal tools.

Good systems:

  • Use cron sparingly and deliberately

  • Let queues absorb pressure

  • Let events carry meaning

  • Combine them where guarantees matter

Bad systems pick one and force everything through it.

Choosing the right trigger is not an implementation detail.
It’s an architectural commitment.

And once you see that clearly, entire classes of problems stop appearing—not because you fixed them, but because you never created them in the first place.


☰ Series Navigation

Core Series

Optional Extras

Understanding Cron from First Principles to Production

Part 1 of 12

A practical series exploring cron from core concepts and architecture to real-world Yii & HumHub implementations, focusing on background jobs, queues, scheduling tradeoffs, and how time-based systems behave in production.

Up next

Idempotency: The Most Important Word in Cron You’re Probably Ignoring

How to design cron jobs that survive retries, overlaps, and partial failures