On this page
- Why background work needs its own discipline
- Scheduled tasks: Cron and its silent failure mode
- Time: The bug that only appears twice a year
- Queues: Moving work out of the request
- Retries and backoff: Recovering without making it worse
- Shutdown and in-flight work: Not losing jobs on deploy
- Monitoring: Making background work visible
- Serverless schedulers: Convenient, with different failure modes
- Checklist
- FAQ
- Related topics
- Sources
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.
- What Is a Cron Job (and Why Do They Fail Silently)? — How the scheduler works and why failure is invisible by default.
- Why Do Cron Jobs Fail Silently (and How to Fix It)? — The specific causes: PATH differences, missing environment variables, permissions, and untracked exit codes.
- How to Monitor Your Cron Jobs — Heartbeats, exit-code checks, and alerting on missed runs.
- What Is Uptime Monitoring? — The same external-check pattern applied to your services.
- How to Set Up Automated Database Backups — The scheduled job where a silent failure costs data.
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.
- Why Should Servers Always Use UTC? — Store and schedule in UTC, convert only at the edges.
- What Is a Cron Job (and Why Do They Fail Silently)? — How cron interprets the time fields you give it.
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.
- How to Build a Background Job Queue — Producers, workers, visibility timeouts, and what to persist where.
- What Is a Webhook? — The other common source of asynchronous work, and why it needs the same guarantees.
- What Is Idempotency (and Why Does It Matter for APIs)? — The property every queue handler needs before it can be retried safely.
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.
- What Is Exponential Backoff? — Spacing out attempts, and when to add jitter.
- How to Add Retry Logic to API Calls — The implementation, including which errors are worth retrying.
- What Is a Dead Letter Queue? — Where messages go when retries are exhausted.
- Dead Letter Queue vs Retry: When to Use Each (and When to Use Both) — The decision, including the pattern that uses both.
- What Is the Circuit Breaker Pattern? — Stopping the retries entirely when a dependency is down.
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.
- What Is Graceful Shutdown? — Finishing in-flight work before exiting.
- Why Does My App Ignore SIGTERM (and How Do I Fix It)? — The signal your container platform sends before it kills you.
- What Is a Health Check? — Telling the platform whether a worker is ready for more work.
- How to Roll Back a Bad Deploy — When the deploy that restarted your workers was itself the problem.
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.
- What Is Observability (and How Is It Different From Monitoring)? — Logs, metrics, and traces, and which answers which question.
- How to Set Up Basic Application Monitoring — The starting stack for a small app.
- How to Get Alerted When Your Site Goes Down — Turning a metric into a notification somebody sees.
- What Is Log Rotation (and Why Do Your Logs Keep Disappearing)? — Why last month’s job output isn’t there when you need it.
- What Are SLA, SLO, and SLI? — Defining “the job worked” in a way you can measure.
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.
- What Is Serverless Computing? — The execution model and where it fits.
- How to Deploy Your First Serverless Function on AWS Lambda — The walkthrough, including scheduled triggers.
- What Are Serverless Cold Starts (and Do They Matter for You)? — Why a function’s first run is slower, and when that matters.
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.
Related topics
- What Is a Cron Job (and Why Do They Fail Silently)?
- Why Do Cron Jobs Fail Silently (and How to Fix It)?
- How to Monitor Your Cron Jobs
- How to Build a Background Job Queue
- What Is a Dead Letter Queue?
- Dead Letter Queue vs Retry: When to Use Each (and When to Use Both)
- What Is Exponential Backoff?
- What Is Idempotency (and Why Does It Matter for APIs)?
- DevOps for AI Builders