Skip to content

One worker at a time: a nightly job with a lock

Three replicas run the nightly report and only one may: a leased lock, and a guard on the transaction so a worker that lost its lock cannot commit.

Updated View as Markdown

The nightly report must be built once, but every replica of the service runs the same scheduler. This is leader election, or a singleton job.

A lock with a lifetime: the worker that gets it builds the report, and the others skip. The report is committed with .guard(lock): the transaction commits only while the lock is still this worker’s. The day in the transactionId is a second fence.

Three replicas run the nightly scheduler at once. Worker a acquires the lock nightly-report and commits the report with guard(lock) and transactionId report-2026-10-24. Workers b and c wait up to a second for the lock, do not get it, and skip. One report is written.Three replicas run the nightly scheduler at once. Worker a acquires the lock nightly-report and commits the report with guard(lock) and transactionId report-2026-10-24. Workers b and c wait up to a second for the lock, do not get it, and skip. One report is written.
One worker builds the report; the guard keeps a late one out.
const lock = queen.lock('nightly-report', { ttl: '30s' })
if (!(await lock.acquire({ wait: '1s' }))) return 'skipped'   // another worker has it
try {
  const report = { day: '2026-10-24', bookings: 412 }
  const result = await queen.transaction()
    .guard(lock)                                  // commits only while the lock is ours
    .queue('daily-reports').push([{ transactionId: `report-${report.day}`, data: report }])
    .commit()
  if (result.success === false) return 'lost the lock'
  return 'done'
} finally {
  await lock.release()
}

Three workers started at once: one built the report and two skipped. There was one report.

Why
a lock expires without telling its holder. A worker paused past the lifetime keeps running, while another one takes the lock. The guard keeps the late worker’s work out: its commit rolls back.

One thing to know: in JavaScript, Python, Go and Rust the lock renews itself in the background while the worker holds it. For long work in JavaScript, pass lock.signal to what the work waits on, so it stops when the lock is lost. For “at most three at a time”, queen.semaphore(name, 3) works the same way.

Built from: KV, locks and timers, Transactions.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close