Every action in a system triggers work. An order is placed, and now there is a confirmation to send, a stock figure to update, a report to refresh, an external system to notify. Each of those is a piece of work, and each carries a placement decision: does it run inline, synchronously, as part of the request the user is waiting on; does it run later, on a scheduled job; or does it go onto a queue to be picked up by a worker. The decision is usually made by whoever writes the code, in the moment, without being named. It is worth naming, because the three options fail in completely different ways.
Inline
Inline work runs inside the triggering request. The user waits for it. Its appeal is simplicity and immediacy: the work is finished by the time the user sees a response, and the result is consistent with the action that caused it. Inline is correct when the work is fast, must be consistent with the action, and the user genuinely needs it done before they continue. It becomes the wrong choice the moment the work is slow or can fail independently. A slow inline step makes the user wait; a failing inline step fails the whole action, including the part that should have succeeded.
Cron
Scheduled jobs run on a timer, decoupled from any request. They are right for work that is naturally periodic, such as a nightly reconciliation or a daily digest, or work that can be batched. The trap with cron is lag and contention. Work placed on a cron does not happen when the triggering event happens; it happens at the next tick, and "the figure is wrong for up to an hour" is a real consequence. Crons also collide: several heavy jobs all scheduled at midnight contend for the same workers and the same locks, turning a quiet hour into the slowest one. Cron is for periodic work, not for deferring event-driven work that someone is actually waiting on.
Queue
A queue takes the work out of the request and hands it to a worker to process as soon as possible, but not synchronously. It is the right answer for work that is triggered by an event, should happen promptly, but does not need to block the user: sending the notification, syncing to the external system, generating the document. A queue also gives you retry, so a transient failure can be retried without failing the original action. The cost is infrastructure and complexity. A queue is another component to run and monitor, and "the work will happen soon" is a weaker promise than "the work is done," so the rest of the system has to be designed to tolerate that gap.
How we decide
- Fast, must be consistent with the action, the user needs it now: inline.
- Naturally periodic or batchable, and a delay of the schedule interval is acceptable: cron.
- Event-driven, should happen promptly, can fail and retry independently, the user does not need to wait: queue.
- The failure question decides more than the speed question: if this work fails, should the triggering action fail with it. If yes, inline. If no, it must not be inline.
The note for the file
Where work runs is invisible while it works and unmistakable when it does not: the four-second save, the figure that is wrong until midnight, the notification that never arrived. Placing the work is a real decision. Make it on purpose, by asking how fast it must be, how periodic it really is, and what should happen when it fails.