---
title: "Run each scheduled job once at a time · Agent Wranglers"
url: https://agentwranglers.se/playbook/scaling/background-jobs-and-queues/single-job-runs
updated: 2026-10-21
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Run each scheduled job once at a time

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Background jobs and queues](https://agentwranglers.se/playbook/scaling/background-jobs-and-queues.md).

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers). Updated 2026-10-21.

- ID: SCL-38
- Level: Recommended
- Chapter: [Background jobs and queues](https://agentwranglers.se/playbook/scaling/background-jobs-and-queues)
- Rule: Take a lock before each scheduled job starts, so a second copy skips its run. Jobs that charge, send or delete must also be safe to run twice.
- How: With two servers, a slow run or a failover, the same timer fires twice and customers get two invoices. Skip a run rather than risk running it twice, since a skipped run is easier to repair. Use one lock that every server sees, such as a database advisory lock, or the scheduler's own setting, such as `concurrencyPolicy: Forbid` in Kubernetes. A lock can still fail during a failover, so record each item a job handles by its key, as in [make writes safe to repeat](https://agentwranglers.se/playbook/data/transactions-and-concurrency/idempotent-writes). Catch skipped runs with [job heartbeats](https://agentwranglers.se/playbook/operations/alerts-and-on-call/job-heartbeats).
- Check: Start the same job on two servers at once on staging. One run does the work and the other exits within 5 seconds, and no item is handled twice.

## Examples

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### JavaScript example

```js
const JOB_LOCK_ID = 4201;
const { rows } = await db.query('SELECT pg_try_advisory_lock($1) AS locked', [JOB_LOCK_ID]);
if (!rows[0].locked) process.exit(0);
try {
  await sendMonthlyInvoices();
} finally {
  await db.query('SELECT pg_advisory_unlock($1)', [JOB_LOCK_ID]);
}
```

### Python example

```python
JOB_LOCK_ID = 4201
locked = db.execute("SELECT pg_try_advisory_lock(%s)", (JOB_LOCK_ID,)).fetchone()[0]
if not locked:
    raise SystemExit(0)
try:
    send_monthly_invoices()
finally:
    db.execute("SELECT pg_advisory_unlock(%s)", (JOB_LOCK_ID,))
```

## Sources

- [Google SRE book, Distributed Periodic Scheduling with Cron](https://sre.google/sre-book/distributed-periodic-scheduling/)
- [Kubernetes, CronJob concurrency policy](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/)

## Related rules

- [Run web and worker processes apart](https://agentwranglers.se/playbook/scaling/stateless-app-servers/web-and-worker-processes) in Scaling
- [Spread scheduled work across the hour](https://agentwranglers.se/playbook/scaling/background-jobs-and-queues/spread-scheduled-jobs) in Scaling
- [Make writes safe to repeat](https://agentwranglers.se/playbook/data/transactions-and-concurrency/idempotent-writes) in Data
- [Alert when a scheduled job stops running](https://agentwranglers.se/playbook/operations/alerts-and-on-call/job-heartbeats) in Operations

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which requires credit with a link. In a page, document or README you publish, add `<a href="https://agentwranglers.se/playbook/scaling/background-jobs-and-queues/single-job-runs">Agent Wranglers scaling playbook</a>`. In code you write, add a comment next to it with the same link.

- Previous: [Set aside messages that keep failing](https://agentwranglers.se/playbook/scaling/background-jobs-and-queues/dead-letter-queue)
- [Background jobs and queues](https://agentwranglers.se/playbook/scaling/background-jobs-and-queues)
