Any work that doesn't need to complete before you respond to the user shouldn't be sitting in the request path — background jobs exist precisely to move that work (sending emails, generating reports, processing uploads) out of the synchronous request/response cycle, where a slow or failing task shouldn't take the user's request down with it.
Background jobs are units of work executed asynchronously, outside the request/response cycle, typically managed by a job queue that handles scheduling, retries, and concurrency. Cron jobs are a related but distinct pattern — work triggered on a fixed schedule (nightly cleanup, weekly reports) rather than in response to a specific event.
Why Background Jobs Matter (and When to Skip Them)
Moving slow or unreliable work out of the request path keeps response times fast and predictable for the user-facing part of the operation, while the background job system handles retries and failure independently — a failed email send shouldn't fail the user's signup request, it should retry on its own schedule without the user ever noticing.
Skip a dedicated job queue for work that's genuinely fast and reliable enough to do synchronously (a quick database write with no external dependency) — introducing async job infrastructure for trivial, fast, reliable work adds unnecessary complexity and a source of eventual-consistency bugs without a real corresponding benefit.
Getting Started with Background Jobs
Enqueuing a job instead of doing the work synchronously in the request handler:
app.post("/signup", async (req, res) => {
const user = await createUser(req.body);
await jobQueue.add("send-welcome-email", { userId: user.id });
res.status(201).json({ user });
});
A worker processing jobs from the queue, with retry configuration:
jobQueue.process("send-welcome-email", async (job) => {
const user = await db.users.findById(job.data.userId);
await emailService.sendWelcomeEmail(user.email);
}, { attempts: 3, backoff: { type: "exponential", delay: 5000 } });
A cron-scheduled job for recurring work:
cron.schedule("0 2 * * *", async () => {
await cleanupExpiredSessions();
});
Core Background Jobs Concepts Every Developer Should Know
Retries need a backoff strategy, not immediate retry. Immediately retrying a failed job (especially one that failed due to a downstream service being overwhelmed) can make the underlying problem worse. Exponential backoff — waiting progressively longer between retry attempts — gives transient failures (a brief network blip, a temporarily overloaded dependency) time to resolve before retrying.
Jobs need to be idempotent, since retries mean a job might execute more than once. The same idempotency principle from API design and event handling applies here — a job that sends an email or charges a card needs to check whether it already succeeded before repeating the side effect on retry.
Dead letter queues capture jobs that fail all retry attempts, so they don't just silently disappear after exhausting retries. Without this, a persistently failing job type can fail invisibly, hiding a systemic issue (a bug, a misconfigured dependency) until someone notices the downstream effect is missing.
Cron scheduling and event-triggered jobs solve different problems and shouldn't be conflated. Cron is for genuinely time-based, recurring work (nightly reports, periodic cleanup); event-triggered background jobs are for work that should happen in response to a specific action (a signup, an upload) but doesn't need to block the response.
Common Background Jobs Mistakes and How to Fix Them
Mistake 1: not making jobs idempotent, causing duplicate side effects (duplicate emails, duplicate charges) when a job retries after a partial failure. Fix: design every job handler to safely handle being executed more than once for the same logical work.
Mistake 2: no dead letter queue or alerting for permanently failed jobs, letting failures disappear silently after retries are exhausted. Fix: route exhausted-retry jobs to a dead letter queue with alerting, so failures are visible and actionable, not silent.
Mistake 3: overloading a single cron job with unrelated tasks, making failures hard to diagnose (which part failed?) and coupling unrelated work's scheduling together. Fix: keep cron jobs focused on one logical task each, with independent scheduling and failure handling.
When Should You Use a Background Job Instead of Doing Work Synchronously?
Use a background job for anything slow, unreliable, or non-critical-path — sending emails, generating reports, processing uploads, calling third-party APIs that aren't required for the immediate response. Do work synchronously when it's fast, reliable, and the result is required before you can meaningfully respond to the request (validating and saving the core data the user is waiting on).
Background Jobs in Production
Set up dead letter queue alerting from the start, since silently failing background jobs are one of the most common ways real bugs go unnoticed for a long time. Also design every job to be idempotent and monitor queue depth/processing lag as a standard operational metric, the same way you'd monitor any other critical system component.
If your signup or checkout flow currently does slow, non-critical work (sending emails, calling third-party APIs) synchronously in the request path, moving that to a background job is a concrete win for both response time and reliability.