On this page
  1. Why background work needs its own discipline
  2. Scheduled tasks: Cron and its silent failure mode
  3. Time: The bug that only appears twice a year
  4. Queues: Moving work out of the request
  5. Retries and backoff: Recovering without making it worse
  6. Shutdown and in-flight work: Not losing jobs on deploy
  7. Monitoring: Making background work visible
  8. Serverless schedulers: Convenient, with different failure modes
  9. Checklist
  10. FAQ
    1. Why do cron jobs fail silently?
    2. Do I need a job queue, or can I run the work inside the request?
    3. How do I stop a background job from running twice?
    4. What should I monitor for a scheduled job?
  11. Related topics
  12. Sources
guide

Background Jobs and Scheduled Tasks: The Part of Your App Nobody Monitors

Cron jobs fail silently, queues lose messages, and retries duplicate work. Learn scheduling, queues, dead letters, backoff, idempotency, and monitoring — with linked guides.

Quick answer

  • AI assistants generate the trigger for background work and skip the guarantees: no monitoring, no retries, no dead-letter handling, and no idempotency.
  • Cron fails silently by design. A job can fail every night for weeks and look healthy, because nothing is checking.
  • A job queue decouples the user’s request from the slow work behind it, but only if the job can survive being delivered more than once.
  • Retries need backoff and a destination for exhausted attempts — otherwise they hammer a broken service and lose the failures that mattered.
  • Every scheduled task and every queue needs a heartbeat, an exit-code check, and a duration ceiling.
  • Every concept below links to a full guide. Start with the monitoring ones, even if the job already works.

Why background work needs its own discipline

Background work is unique in that nobody is waiting for it. A user-facing bug gets reported within minutes because someone saw it; a nightly job that stopped writing records is discovered weeks later, when a report doesn’t add up. The absence of a person in the loop is exactly what makes failure silent.

AI coding assistants are good at the visible half of this. Ask for a nightly cleanup job and you’ll get a schedule expression and a handler function. Ask for a background queue and you’ll get a producer, a worker, and a dequeue loop. What’s missing is everything around the happy path: what happens when the job throws, when the worker dies mid-task, when the external API is down for three hours, or when the same message is delivered twice.

The guides below cover that surrounding infrastructure: scheduling and its failure modes, queues and retries, idempotency, graceful shutdown, and the monitoring that makes any of it visible.


Scheduled tasks: Cron and its silent failure mode

Cron is the scheduler on every Linux machine, and its defining trait is that it doesn’t care about outcomes. It launches your command at the appointed time, captures whatever output the command produces, and discards it in a way you probably aren’t reading. Exit codes are recorded but not surfaced. That’s why “why do my cron jobs fail silently” and “how do I monitor my cron jobs” are the two questions that matter.


Time: The bug that only appears twice a year

Scheduled work is expressed in time, and time is a minefield: servers in one timezone, users in another, daylight saving shifts that skip or repeat an hour, and job runtimes expressed in the wrong zone. Standardizing on UTC internally removes most of it, and being explicit about the timezone of a schedule removes the rest.


Queues: Moving work out of the request

A queue exists to break the link between a user’s request and the work it triggers. The request enqueues a message and returns; a worker processes it later, with its own retry policy and its own failure handling. That decoupling is what lets you survive a slow third-party API without timing out the user — and it’s what introduces duplicate deliveries, poison messages, and ordering questions.


Retries and backoff: Recovering without making it worse

Retrying is the simplest resilience pattern and the easiest to get wrong. Retry immediately and a struggling dependency gets hammered by every client at once; retry forever and a permanently broken message blocks the queue; retry a write without idempotency and you create duplicates instead of recovering.


Shutdown and in-flight work: Not losing jobs on deploy

Deploys restart processes. If a worker is killed mid-job, the work is either lost or redelivered depending on how the queue is configured — and both outcomes need to be intentional. Handling the termination signal, draining in-flight work, and letting the platform know when you’re done is what makes a deploy uneventful for background processing.


Monitoring: Making background work visible

The rule for background work is that every job needs to prove it ran. That’s a different monitoring posture from request-path monitoring, where traffic itself is the heartbeat. For scheduled jobs and queue consumers, silence is ambiguous — it means success, or it means the scheduler was disabled, the worker crashed, or the queue is empty because the producer broke.


Serverless schedulers: Convenient, with different failure modes

Running scheduled work as a function removes the server you’d otherwise patch, and adds a few properties worth knowing: execution time limits, cold starts that push a job past its window, and the fact that the platform’s scheduler is now a dependency you don’t control. The failure modes change shape rather than disappearing.


Where this bites vibecoders

The request was “send a weekly digest email to users.” The assistant returns a mailer function and a line in the deploy config that schedules it. Nothing captures the output, nothing checks the exit code, and nothing alerts if the run doesn’t happen. For six weeks the digest goes out and everyone is happy. Then the mailing provider changes an authentication requirement, the job starts failing on the first send, and the only trace is an exit code in a log file nobody opens. The users who didn’t get an email assume you stopped sending it; you assume they lost interest. This is the default shape of background-work failures — the absence of a signal rather than the presence of an alarm. When you accept a scheduled task from an assistant, the job isn’t the handler function: it’s the handler, the output capture, the exit-code check, the heartbeat, and the alert that fires when the run is missing.

Checklist

  • Alert when a scheduled job doesn’t run, not only when it fails.
  • Capture stdout and stderr from every scheduled command somewhere you can read later.
  • Set a timeout so a hung job can’t overlap with its next run.
  • Store and schedule everything in UTC; convert timezones at the display layer only.
  • Make every queue handler idempotent before enabling retries.
  • Add backoff and a dead-letter destination; never retry forever.
  • Drain in-flight jobs on shutdown and exit only when they’re finished.
  • Log a job’s start, end, duration, and outcome — those four fields answer most incidents.
  • Restore-test the backup job, because “the job ran” and “the data is recoverable” are different claims.

FAQ

Why do cron jobs fail silently?

Because nothing is watching them. Cron records a job’s exit code, writes output to a file or an email nobody reads, and moves on. A job that has failed every night for three weeks looks exactly like a job that works, right up until someone notices the data is missing. Capturing output, checking exit codes, and alerting on missed runs is what turns a silent failure into a notification.

Do I need a job queue, or can I run the work inside the request?

Run it inline while it takes under a second and the caller doesn’t need the result. Move it to a queue when the work is slow, when it calls an external service that can time out, when it needs retries, or when a failure shouldn’t fail the user’s request. Queues exist to decouple the user’s response from the work, so add one when those two things stop belonging together.

How do I stop a background job from running twice?

Make it idempotent: design the operation so performing it twice has the same effect as performing it once. That usually means a unique constraint on the record being created, an upsert instead of an insert, or a check that the work was already completed. Retries, duplicate deliveries, and overlapping schedules are all normal, so correctness has to survive them.

What should I monitor for a scheduled job?

Three things: that it ran, that it succeeded, and that it finished in a reasonable time. A heartbeat that alerts when a run doesn’t happen catches the job that was deleted or disabled; exit-code checks catch failures; duration tracking catches a job that is quietly growing toward its next run. Monitoring only the failure case still leaves you blind to a job that never started.


Sources

Share: