Invental/Review lessons/Background jobs when something fails

7 lessons · 11 checks

Background jobs when something fails

Queues and workers fail in quiet ways: a claim nobody releases, a retry that re-sends to everyone, a job that schedules itself forever. Each lesson below is one failure mode, with the pattern that closes it.

Major

One retry step per side effect

Moving a side effect into a retried job turns every “send to all” into a duplicate risk and every payload into stale data: make each send its own retry step and re-check state when the job runs, not when it was queued.

Read the lesson →
Major

Release a claim when the work fails

A “claim” flag that guards a side effect must be released when the side effect fails; otherwise the retry that is meant to recover the work is the thing that skips it.

Read the lesson →
Major

Finishing a leased job must check the lease

A job lease is only safe if finishing the job checks that you still hold it: otherwise the slow worker you already replaced wakes up and overwrites the new worker's result.

Read the lesson →
Major

Never advance a cursor past pending work

One “read up to N” number cannot describe a channel where some records are handled immediately and others wait: the cursor must never pass the oldest record that still owes a delivery.

Read the lesson →
Major

Redis hash tags don’t make KEYS cheap

A Redis hash tag decides which node holds a key; it does not make KEYS cheap, so keep an index or counter for any key family you need to enumerate.

Read the lesson →
Major

Queue dedup ignores jobs already running

A queue's “deduplicate by job id” usually only covers jobs still waiting. Once a job is active, a second trigger needs your own in-flight check, or it becomes a duplicate run.

Read the lesson →
Major

Self-requeueing jobs need their own limit

A job that re-enqueues itself needs its own attempt counter in the payload: a new job starts with fresh retries, so the queue's retry limit no longer protects you from a batch that always fails.

Read the lesson →

The checks, with the lesson behind each

If your email job crashes on the third of five recipients and retries, do the first two get the email twice?+
Moving a side effect into a retried job turns every "send to all" into a duplicate risk and every payload into stale data: make each send its own retry step and re-check state when the job runs, not when it was queued.
If the call right after you set a "processing" or "done" marker fails, does a retry still do the work?+
A "claim" flag that guards a side effect must be released when the side effect fails; otherwise the retry that is meant to recover the work is the thing that skips it.
When a background worker finishes a job, does its final write verify it still owns the job?+
A job lease is only safe if finishing the job checks that you still hold it: otherwise the slow worker you already replaced wakes up and overwrites the new worker's result.
If your worker crashes right now, can a message that arrived but was not yet processed be skipped on restart?+
One "read up to N" number cannot describe a channel where some records are handled immediately and others wait: the cursor must never pass the oldest record that still owes a delivery.
Does any background job in your app call Redis KEYS (or an unbounded scan) on the same instance your requests use?+
A Redis hash tag decides which node holds a key; it does not make KEYS cheap, so keep an index or counter for any key family you need to enumerate.
If a user double-clicks "Upgrade" (or two admins click it), how many upgrade jobs run?+
A queue's "deduplicate by job id" usually only covers jobs still waiting. Once a job is active, a second trigger needs your own in-flight check, or it becomes a duplicate run.
Do any of your background jobs schedule a follow-up job of themselves? What stops that chain if the same step keeps failing?+
A job that re-enqueues itself needs its own attempt counter in the payload: a new job starts with fresh retries, so the queue's retry limit no longer protects you from a batch that always fails.

More checks from the same reviews

Yes/no questions from other reviews on this theme. Each one is quick to answer for your own app.

  1. For each capped batch job you run, what happens to item number cap + 1, and will it ever be processed?
  2. If a job is scheduled for tomorrow, can any of your expiry or cleanup jobs kill it before it ever runs?
  3. In your background jobs, can a failure to write an audit/history/status row mark a successful job as failed?
  4. Does your webhook handler deduplicate by event id rather than by timestamp?

Want a senior reviewer to run these checks on your code? Ongoing pull-request review capacity →

Want these checked on your code?

We run reviews like these on real codebases. Tell us what you are building and we confirm scope before anything starts.

— Get in touch
hi@invental.co ↗
— Or
— What to include

A link or short description of the code or system, what you want checked, your stack, and when you need the answer. No repository access is needed until scope is agreed.

— Or leave a note
We reply within one business day.