---
title: "One worker at a time: a nightly job with a lock"
description: "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."
---

> Queen MQ documentation, for AI agents
> Complete self-contained summary of Queen MQ: https://queenmq.com/llms-brief.txt
> Fetch that first when the question is about the product rather than about this page.
> Index of all pages: https://queenmq.com/llms.txt

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

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.

**Figure.** 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.

JavaScript:

```js
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()
}
```

Python:

```python
lock = queen.lock("nightly-report", ttl_seconds=30)
if not await lock.acquire(wait=1):                # another worker has it
    return "skipped"
try:
    report = {"day": "2026-10-24", "bookings": 412}
    result = await (
        queen.transaction()
        .guard(lock)                              # commits only while the lock is ours
        .queue("daily-reports")
        .push([{"transactionId": f"report-{report['day']}", "data": report}])
        .commit()
    )
    if not result:
        return "lost the lock"
    return "done"
finally:
    await lock.release()
```

Go:

```go
lock := client.Lock("nightly-report", 30*time.Second)
wait, cancel := context.WithTimeout(ctx, time.Second) // wait at most 1 s for it
defer cancel()
if err := lock.Acquire(wait); errors.Is(err, context.DeadlineExceeded) {
	return "skipped", nil // another worker has it
} else if err != nil {
	return "", err
}
defer func() { _, _ = lock.Release(ctx) }() // if this fails, the lock expires with its ttl

day := "2026-10-24"
report := map[string]interface{}{"day": day, "bookings": 412}
result, err := client.Transaction().
	Guard(lock). // commits only while the lock is ours
	Queue("daily-reports").
	Push(queen.PushItem{TransactionID: "report-" + day, Payload: report}).
	Commit(ctx)
if err != nil {
	return "", err
}
if result.IsKVPrecondition() {
	return "lost the lock", nil
}
return "done", nil
```

Rust:

```rust
let lock = queen.lock("nightly-report", Duration::from_secs(30));
if !lock.acquire_within(Duration::from_secs(1)).await? {
    return Ok("skipped"); // another worker has it
}
let day = "2026-10-24";
let committed = queen
    .transaction()
    .guard(&lock) // commits only while the lock is ours
    .push_item(TxnPushItem {
        queue: "daily-reports".into(),
        partition: None,
        payload: json!({ "day": day, "bookings": 412 }),
        transaction_id: Some(format!("report-{day}")),
        trace_id: None,
    })?
    .commit()
    .await;
lock.release().await?; // whatever happened, give the lock back
if !committed?.success {
    return Ok("lost the lock");
}
Ok("done")
```

PHP:

```php
$lock = $queen->lock('nightly-report', ['ttlSeconds' => 30]);
if (!$lock->acquire(1.0)) {                     // waits up to 1 s for it
    return 'skipped';                           // another worker has it
}
try {
    $report = ['day' => '2026-10-24', 'bookings' => 412];
    $result = $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 {
    $lock->release();
}
```

C++:

```cpp
queen::Lock lock = client.lock("nightly-report", 30);
if (!lock.acquire(1000)) return "skipped";  // another worker has it
json result;
try {
    json report = {{"day", "2026-10-24"}, {"bookings", 412}};
    std::string id = "report-" + report["day"].get<std::string>();
    result = client.transaction()
        .guard(lock)  // commits only while the lock is ours
        .queue("daily-reports").push({{{"transactionId", id}, {"data", report}}})
        .commit();
} catch (...) {
    lock.release();
    throw;
}
lock.release();
return result.value("success", false) ? "done" : "lost the lock";
```

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](/writing-applications/kv-locks-timers/#locks-and-semaphores), [Transactions](/writing-applications/transactions/).

Source: https://queenmq.com/patterns/one-worker/index.mdx
