Home › High-Level Design › Design a Distributed Job Scheduler
D

Design a Distributed Job Scheduler

We are building the thing that fires jobs at the right time, at scale, without losing them and without running them twice in a way that hurts. It looks like a two-minute whiteboard sketch and then three nasty problems fall out of the box, and the candidates who pass spend their 45 minutes on the one that is actually hard.

High-Level DesignHard16 min read

If you remember five things

The whole answer in one glance. Everything below is the defense of these five calls.

  1. Split scheduling from execution. A scheduling plane decides what fires and a dispatch plane fires it, with a durable time-bucketed queue between them so a slow handler never stalls the clock.
  2. Shard by job_id, lease per shard. One owner per shard via leader election, not one global leader. Real parallelism, even load, no central bottleneck.
  3. At-least-once plus idempotency. Exactly-once execution over a network is a fantasy. Aim for exactly-once effects: a stable key per scheduled instant plus lease fencing make duplicates harmless.
  4. Smear the midnight spike. Deterministic jitter plus a look-ahead window plus per-tenant rate limits turn 230k fires in one tick into a flat drain.
  5. Durability beats latency. A job three seconds late is a shrug. A job that never fires is a refund and an incident review. Every call below ranks durability first.

1Clarifying the requirements#

Pin down what "job scheduler" means here before drawing a box, because the answer changes the whole design.

  • A cron engine and a "charge this card in 7 days" engine look identical on a napkin.
  • They diverge hard the moment durability enters the picture.
  • So I spend the first two minutes narrowing scope, not sketching.

Candidate: When you say scheduler, do you mean cron-style recurring jobs, one-shot delayed tasks like "charge this card in 7 days," or both?

Interviewer: Both. Users submit one-time jobs with a run-at timestamp and recurring jobs with a cron expression.

Candidate: What does a job actually do when it fires? Are we executing arbitrary user code inside our fleet, or are we just emitting a trigger to some downstream worker?

Interviewer: Assume the scheduler's responsibility ends at reliably invoking a handler: an HTTP callback or dropping a message on a queue. The user's business logic runs in their own workers.

Candidate: Good, that keeps us out of the sandboxing business. Execution guarantee: is at-least-once acceptable, or do we need exactly-once?

Interviewer: At-least-once is the floor. We want strong tools to make duplicate runs safe, but you cannot promise exactly-once over a network. Good.

Candidate: Scale and timing precision? Are we okay with a job firing a few seconds late, or is this sub-second?

Interviewer: Hundreds of millions of scheduled jobs. Second-level precision is fine for most. Flag anything that needs tighter.

What those answers just told me:

  • "Handler invocation only" keeps me out of sandboxing and makes the queue the real product.
  • "At-least-once is the floor" means the whole game is making duplicates safe, not avoiding them.
  • "Second-level precision" plus "hundreds of millions of jobs" is the interviewer quietly handing me the thundering herd to design around.

Functional requirements

  • Submit a one-time job with a run-at time, or a recurring job with a cron schedule.
  • Cancel or update a job before it fires.
  • Execute by calling an HTTP endpoint or enqueuing a message, with configurable retries and backoff.
  • At-least-once delivery, with an idempotency key passed to the handler so the user can dedupe.
  • Status and run history per job.

Non-functional requirements

  • Durability first. A scheduled job must survive node and datacenter loss. Losing a "charge the customer" job is a business incident, not a blip.
  • Horizontally scalable to hundreds of millions of pending jobs and tens of thousands of fires per second at peak.
  • No single point of failure. A dead scheduler node should cause a brief delay, not dropped jobs.
  • Bounded lateness. P99 fire latency within a few seconds of the target time.
  • Multi-tenant fairness. One customer's million-job burst should not starve everyone else.
From the interviewer's chair

I am ranking durability above latency on purpose and I want to hear you do the same. A job that fires three seconds late is a shrug. A job that never fires is a refund and an incident review. If you optimize for p99 before you have made "never drop a fire" true, I have quietly downgraded you. Name the hard part (safe duplicates at a spike) and go spend your time there.

2Back-of-the-envelope estimation#

Get the scale in your head, because one number decides the architecture. I will flag it when I reach it.

The point of this section

You are not here to compute storage to three decimals. You are here to surface one load-bearing number: the fires are not spread evenly, they pile into a single tick at the top of the hour. That pileup, not the average, is what the design has to survive. Find it fast, then move on.

Pending set and average rate.

  • Say 200M pending jobs at any moment, mostly recurring with fewer one-shots.
  • If recurring jobs run roughly hourly and one-shots are a minority, call it 2 billion fires per day.
  • 2B over 86,400s is about 23,000 fires/second on average, which is comfortable.

The spike (this is the one that matters).

  • Humans love round numbers: everyone schedules for :00 of the hour, midnight UTC, the top of the minute.
  • Assume peak is 10x the average inside a one-second window: about 230,000 fires/second at the worst tick.
  • Store jobs and poll "what is due now," and at midnight a thundering herd slams a single tick.
  • The architecture has to smear that load across time, not swallow it in one gulp.

Storage.

  • 200M jobs at roughly 1 KB each (metadata, cron string, payload pointer, status) is about 200 GB for the live set, which fits a sharded store.
  • Run history at 2B fires/day, 7-day retention, ~300 bytes per record is 2B x 7 x 300 bytes, roughly 4.2 TB.
  • History goes to cheap append-only storage, never the hot path.

Bandwidth on fire.

  • If each fire sends a 2 KB payload callback, 230k/s x 2 KB is about 460 MB/s out of the dispatch tier at peak.
  • Fine for a horizontally scaled fleet of dispatchers behind connection pools.
~230k
The number that shapes the architectureFires concentrated into a single tick at 00:00:00 UTC, 10x the 23k/s average. Everything below is about absorbing that spike without melting the handlers or the store. Remember this one; it comes back in every trade-off.

3Core APIs#

Keep these small and boring on purpose. The interesting design hides in two fields, and I will point at them.

POST /v1/jobs
Authorization: Bearer <token>
Content-Type: application/json

{
  "type": "recurring",
  "schedule": "0 */1 * * *",
  "timezone": "America/New_York",
  "target": { "kind": "http", "url": "https://api.tenant.com/hook", "method": "POST" },
  "payload": { "report": "daily" },
  "retry": { "max_attempts": 5, "backoff": "exponential" },
  "idempotency_key_template": "job-{job_id}-run-{scheduled_ts}"
}
# Response
201 Created
{ "job_id": "jb_8fh2", "next_run_at": "2026-11-03T15:00:00Z", "status": "scheduled" }
GET    /v1/jobs/{job_id}          # current state + next_run_at
DELETE /v1/jobs/{job_id}          # cancel; soft-delete, tombstone
PATCH  /v1/jobs/{job_id}          # update schedule or payload
GET    /v1/jobs/{job_id}/runs     # paginated run history

Two design choices worth defending:

  • The idempotency key template is load-bearing, not decoration. Because we are at-least-once, the key the handler sees has to be stable for a given scheduled instant. I compute it from job_id plus scheduled_ts, never a per-attempt UUID, so a retry or an accidental duplicate carries the exact same key and the user dedupes cleanly. A per-attempt UUID would defeat the whole point.
  • Cancel is a soft delete (tombstone), not a hard delete. A fire already in flight when the cancel lands can check the tombstone and bail, instead of racing a hard delete.

4High-level design#

The point of this section

The box diagram is table stakes. What earns points is the single seam: a durable time-bucketed queue between the plane that decides what fires and the plane that fires it. That seam lets schedulers enqueue fast and cheap while dispatchers drain at a controlled, rate-limited pace. It is the thing that smears the midnight spike. Draw the boxes fast, then defend that seam.

The spine is two planes with a queue between them, so draw it fast:

  • A scheduling plane decides what should fire and when.
  • An execution plane actually fires it and tracks the result.
  • A durable queue sits between them so a slow or failing handler never backs up the clock.
  • Jobs are partitioned into shards; scheduler nodes claim shards via leader election so exactly one node owns a shard at a time.
flowchart LR U[Client] --> API[Job API] API --> JDB[(Job Store)] API --> META[Metadata Cache] SCH[Scheduler Nodes] --> JDB SCH --> LEASE[(Coordination Store)] SCH --> RQ[Ready Queue] RQ --> DISP[Dispatcher Pool] DISP --> HOOK[Tenant Handler] DISP --> HIST[(Run History)] DISP --> RQ
The whole system at a glance. The seam that matters is the Ready Queue between the scheduler nodes and the dispatcher pool: schedulers enqueue cheaply, dispatchers drain at a controlled rate, and the dispatcher loops back into the queue so retries ride the same durable path.

Write path.

  • The client hits the Job API, which validates the request.
  • It computes the first next_run_at from the cron expression plus timezone.
  • It writes the row to the durable Job Store (the source of truth) and returns.
  • Nothing fires yet, and that is the point. The write path is boring and durable on purpose.

Read and trigger path.

  • Scheduler nodes continuously look ahead a short window (say the next 60 seconds) inside their owned shards.
  • They pull jobs whose next_run_at falls in that window and enqueue them into the ready queue, keyed by second.
  • Dispatchers pull from the current second's bucket, call the handler, and record the result.
  • For recurring jobs the dispatcher computes the next occurrence and writes it back.
  • If a handler fails, the dispatcher re-enqueues into a delayed bucket with backoff: retries loop back through the same durable path.

Why split it this way.

  • Scheduling is a steady scan; dispatching is spiky and latency-sensitive. Different shapes, different scaling.
  • The queue decouples "the job is due" from "the handler responded," so a slow handler never stalls the clock.
  • It gives me the one lever that tames the spike: schedulers enqueue fast, dispatchers consume at a pace I control.

5Data model#

Use a few stores, each picked for its access pattern. Jobs and run history are different beasts, so I do not force them into one layout.

The due-scan is the access pattern that decides everything.

  • Two patterns dominate: point lookups by job_id, and a range scan that says "give me jobs in shard S due before time T."
  • That second one is a hot, latency-sensitive range query running constantly alongside next_run_at updates.
  • Pick the wrong primary key layout and the scan becomes a full-table crawl that falls over at the spike.
  • So the partition key is the shard and the clustering key is next_run_at: the due-scan is a cheap ordered range read, not a scan-and-filter.

Store choice: a horizontally partitioned SQL store or a strongly consistent wide-column store (sharded Postgres, CockroachDB, or Cassandra and DynamoDB used carefully). I lean toward strong per-key consistency, because the next_run_at update after a fire must not race a second dispatcher.

jobs (source of truth, partitioned by shard, clustered by next_run_at):

field type notes
job_id string (PK) ULID, globally unique
shard_id int hash(job_id) mod N, partition key
type enum one_time or recurring
schedule string cron expr, null for one_time
timezone string IANA tz, matters for DST
next_run_at timestamp clustering key, indexed
payload_ref string pointer to blob store for big payloads
target json http url or queue topic
retry_policy json max attempts, backoff
status enum scheduled, running, paused, cancelled
version int optimistic concurrency token
lease_owner string dispatcher that claimed this run
lease_until timestamp fencing deadline

runs (append-only, cheap storage, not the hot path):

field type notes
run_id string (PK)
job_id string
scheduled_ts timestamp the instant this run was meant to fire
attempt int 1..max_attempts
outcome enum success, failure, timeout
idempotency_key string what the handler saw
latency_ms int fire time minus scheduled_ts
  • Jobs need transactional updates and ordered range scans; runs are write-heavy, read-rarely, and perfect for a log-structured or columnar store.
  • Big payloads go to object storage behind payload_ref, so the hot row stays near 1 KB and the due-scan stays fast.
  • I keep latency_ms on every run row because it is my single best health signal: it is literally fire time minus scheduled time, and it is the thing that spikes first when the design is under stress.

6Deep dives#

6.1 At-least-once execution and exactly-once effects (the signature hard part)#

This is the heart of the problem. You cannot get exactly-once execution across a network, full stop.

  • A dispatcher calls the handler, the handler runs, then the dispatcher crashes before recording success.
  • On recovery we genuinely do not know whether it ran.
  • Skip it and that is at-most-once: we might drop a payment.
  • Retry it and that is at-least-once: we might double-charge.
  • At-least-once is the only honest choice, so the real work is making duplicates harmless. I say this to the interviewer directly: the goal is exactly-once effects, not exactly-once execution.
Pause before you read on: a dispatcher claims a job, pauses on a long GC past its lease, and wakes up still believing it owns the job. Another dispatcher has already reclaimed and run it. What stops the zombie from writing a stale "done"?
Reveal the answer
The fencing token. The lease claim is a compare-and-set on the version column, so the zombie's write carries an old version and the CAS fails. Lease expiry alone would let both the GC zombie and the new owner commit, which is the classic lease bug. The version is what makes the second writer lose the race structurally, not by luck. Reaching for fencing unprompted is the staff-level signal here.

Three mechanisms working together get us to safe duplicates.

  • 1. Lease with fencing. When a dispatcher claims a due job it writes lease_owner and lease_until = now + 30s under optimistic concurrency using the version column. Only the current holder may mark the run complete. If it dies, the lease expires and another dispatcher reclaims it. The version is the fence: a dispatcher that paused on a long GC and woke up still thinking it owns the job cannot write a stale result, because its version is behind and the CAS fails.
  • 2. Idempotency key handed to the handler. Derived from job_id plus scheduled_ts, so every attempt for the same scheduled instant carries an identical key. The handler is contractually expected to dedupe on it. This is how "charge card" becomes safe: the payment service sees the same key on the duplicate and refuses the second charge. We do not make the effect exactly-once ourselves, we hand the downstream the tool to do it.
  • 3. Transactional state transition. Marking a run done and computing the next occurrence for a recurring job happen in one atomic write. Either the run is recorded and next_run_at advances, or neither does. There is no window where a job is both "done" and "still due," which would otherwise produce a phantom second fire on the next scan.
sequenceDiagram participant SCH as Scheduler participant RQ as Ready Queue participant D as Dispatcher participant JDB as Job Store participant H as Handler SCH->>RQ: enqueue due job with scheduled_ts D->>RQ: pull job D->>JDB: claim lease CAS on version JDB-->>D: lease granted version 7 D->>H: invoke with idempotency key H-->>D: 200 ok D->>JDB: mark done and set next_run_at CAS version 7 JDB-->>D: committed version 8
The fire path for one job. The lease claim and the final "mark done, advance next_run_at" are both compare-and-set on the same version, so a stale or duplicate dispatcher loses the race and the state transition stays atomic.

Missed and overlapping runs are the two failure modes interviewers love to probe.

  • Missed runs. If the whole scheduler fleet was down for 10 minutes, on recovery a shard's scan finds jobs whose next_run_at is in the past. For a recurring job I usually fire once for the most recent missed slot and skip the backlog ("catch up at most one"), because replaying 10 minutes of hourly jobs is almost never what the user wants. Configurable per job.
  • Overlapping runs. A long-running recurring job can have its next fire arrive before the previous one finished. I guard with a per-job concurrency flag: allow_overlap: false means the scheduler will not enqueue the next instance while a lease on the current run is still live.
Why this earns points

Almost every candidate says "at-least-once" and stops. The senior move is to name out loud that exactly-once execution over a network to an arbitrary tenant handler is a fantasy, then show the three-part machinery (fence, stable key, atomic transition) that makes duplicates harmless. That reframe, from "avoid duplicates" to "make duplicates safe," is the whole interview.

Trade-off I am stating on purpose.

  • I chose at-least-once with idempotency over building a two-phase-commit "exactly-once" fire.
  • 2PC across our scheduler and an arbitrary tenant handler is fragile and slow.
  • Most tenant handlers simply cannot participate in a distributed transaction we control.
  • Pushing dedupe to a stable idempotency key is cheaper, degrades gracefully, and actually works in production.

6.2 Leader election, sharding, and absorbing the spike#

The scheduling plane needs exactly one owner per shard, and it has to rebalance when nodes join or die.

  • I keep a coordination store (etcd, ZooKeeper, or a Raft group) holding a lease per shard.
  • A scheduler node acquires a shard lease, renews it every few seconds, and if it fails to renew, the lease expires and another node grabs it.
  • This is leader election at shard granularity, not one global leader, so I get real parallelism and no single scheduling bottleneck.
flowchart TD COORD[(Coordination Store)] --> L0[Shard 0 lease] COORD --> L1[Shard 1 lease] COORD --> L2[Shard 2 lease] N1[Scheduler A] --> L0 N1 --> L1 N2[Scheduler B] --> L2 N1 --> JDB[(Job Store)] N2 --> JDB
Shard ownership. Each shard has exactly one lease in the coordination store, and scheduler nodes hold a subset. A node death expires its leases and others claim them, so a failure is a brief rebalance, not a dropped shard.

Why shard by hash(job_id).

  • It spreads load evenly and makes ownership a pure function of the key: any node computes which shard owns a job without asking anyone.
  • The downside: a tenant's jobs scatter across shards. That is good for fairness (no shard becomes a per-tenant hotspot) but means per-tenant rate limiting has to be coordinated separately, it does not fall out of the sharding for free.

Now the spike, which is the reason this whole plane exists.

  • At 00:00:00 every shard's look-ahead scan surfaces a pile of due jobs at once.
  • Fire them all in the same second and you melt the handlers and the queue together.
  • Two defenses, used together:

  • Time-bucketed queue with jitter. The ready queue is partitioned by second-bucket. Schedulers enqueue into the exact target second and dispatchers drain at a per-tenant rate limit. For jobs that only need "approximately on the hour," I add a small deterministic jitter derived from job_id (say 0 to 5 seconds), computed at scheduling time so the herd self-spreads across six buckets instead of slamming one. Jobs that genuinely need exact timing opt out and pay for a reserved-rate lane.

  • Look-ahead window. Schedulers scan the next 60 seconds, not just "now." This pre-loads the queue so the dispatch tier has a steady runway instead of hitting a cliff at each tick. Scanning ahead also hides Job Store read latency, since the read happens well before the deadline rather than on the critical path at T.

The combination turns 230k-in-one-tick into a roughly flat drain: jitter spreads the herd, buckets sequence it, and the per-tenant rate limit caps what any one customer can inflict.

6.3 Reliable triggering without losing or duplicating the enqueue#

There is a subtle gap between "the scheduler decided this job is due" and "it is safely sitting in the ready queue."

  • Enqueue, then crash before recording the enqueue: on restart it re-scans and enqueues again, a duplicate.
  • Record the hand-off, then crash before enqueuing: the job is marked handed-off but never reached the queue, a lost fire.
  • This is the classic dual-write problem, and losing a fire is the one outcome my durability-first requirement forbids.

The clean fix is the transactional outbox.

  • The scheduler, in one transaction against the Job Store, advances a handoff_ts and appends a row to an outbox table. Both land or neither does.
  • A relay process tails the outbox, pushes to the ready queue, and marks outbox rows done only after a confirmed enqueue.
  • The queue hand-off is itself at-least-once, so a duplicate can still reach the dispatcher. That is fine by design: the lease CAS means the second copy loses the race to claim and gets dropped.
  • So the invariant holds end to end: never lose a fire, collapse any duplicate at the lease.
sequenceDiagram participant SCH as Scheduler participant JDB as Job Store participant OBX as Outbox participant RLY as Relay participant RQ as Ready Queue SCH->>JDB: tx advance handoff_ts and append outbox row JDB-->>SCH: committed RLY->>OBX: tail new rows RLY->>RQ: push due entry RQ-->>RLY: enqueue confirmed RLY->>OBX: mark row done
The outbox hand-off. The hand-off marker and the outbox row commit atomically, and a separate relay pushes to the queue. A crash anywhere just replays at-least-once, and the dispatcher's lease collapses the duplicate.

This is the posture of the whole design: layered redundancy.

  • Every hand-off is at-least-once, and every consumer is idempotent.
  • Correctness survives a crash at any single step.
  • I would rather have three cheap at-least-once hops that each dedupe than one clever "exactly-once" hop that nobody can actually build.

7Bottlenecks and trade-offs#

The due-scan on the Job Store breaks first.

  • At 230k due jobs in a tick spread over many shards, the range reads plus the next_run_at write-back are a write-amplification problem, because every fired recurring job is also a write.
  • Keep the hot index on (shard_id, next_run_at) tight, batch the look-ahead reads, and batch the next_run_at updates into grouped writes.
  • If a single shard still gets hot, split it. Shard count should be generous from day one (in the thousands), so rebalancing moves a thin slice and stays cheap.

The coordination store is a risk if you overuse it.

  • etcd and ZooKeeper are great but not infinitely scalable, and people kill them by routing too much through them.
  • I keep traffic tiny: only shard leases and renewals, never per-job state. Per-job coordination lives in the Job Store, not the consensus layer.
  • If the coordination store blips, running leases keep their grace period, so a short outage is a brief rebalance pause, not dropped jobs.

Single points of failure: none by design.

  • Schedulers are a pool, dispatchers are a pool, the Job Store is replicated with a quorum, and the coordination store is a Raft or ZAB quorum.
  • The closest thing to a SPOF is the ready queue, so I run it as a replicated, partitioned log (Kafka-style), where a broker loss does not lose buckets.

Consistency call I made on purpose.

  • Strong consistency on the fire path (lease CAS, transactional state transitions), eventual on run history and status reads.
  • A dashboard showing a run as "running" for a second after it finished is a perfectly acceptable lie.
  • I will not trade correctness on the fire path to make a dashboard snappier.

Clock skew.

  • Schedulers decide "due" on their own clocks, so skew means a job could fire a second early or late.
  • I rely on NTP-synced clocks and treat the 60-second look-ahead window as the skew buffer.
  • For tighter bounds I would pin a job to a single time-authority, but I would not pay that cost for everyone when second-level precision is the stated norm.

Fairness and noisy neighbor.

  • Per-tenant token buckets at the dispatcher, so one tenant's million-job midnight burst drains at their own rate limit while everyone else keeps flowing.
  • The queue is tenant-aware, so a backed-up tenant cannot head-of-line block others sharing a partition.

Rank durability above latency on the fire path, then spend the consistency budget in one narrow place.

8Summary#

The decisions that matter, the ones I would lead with on a recap:

  • Separate scheduling from execution with a durable time-bucketed queue, so a slow handler never stalls the clock and the midnight spike smears across buckets instead of landing in one tick.
  • Shard by job_id with per-shard leader election, for parallelism, even load, and no global bottleneck.
  • At-least-once plus idempotency keys and lease fencing for exactly-once effects, because exactly-once execution over a network to arbitrary handlers is a fantasy and saying so is the senior move.
  • Outbox hand-off to kill the dual-write gap between "decided due" and "safely queued," with the lease collapsing any duplicate that slips through.
  • Strong consistency on the fire path, eventual on reads, a deliberate and narrow place to spend the consistency budget.
  • If I had more time: adaptive sharding that splits hot shards from live metrics, a dead-letter queue with alerting for jobs that exhaust retries, exact-timing priority lanes with reserved dispatcher capacity, and a what-if simulator so tenants can preview their next 24 hours before scheduling a herd into 00:00.
Follow-ups the interviewer will ask (and your answers)
"The dispatcher crashes after the handler runs but before it records success. Double-fire?"
Possibly a second invocation, yes, and that is fine. The lease expires, another dispatcher reclaims and re-invokes with the same idempotency key (derived from job_id plus scheduled_ts), and the handler dedupes on it. We give at-least-once execution and exactly-once effects. We never pretend to avoid the duplicate, we make it harmless.
"A dispatcher pauses on a long GC past its lease, then wakes up and writes 'done.' What happens?"
Its write loses. Every state transition is a compare-and-set on the version column, so the zombie carries a stale version and the CAS fails. The new owner already advanced the version. Lease expiry alone would not save you here, the fencing token is what makes the stale writer lose structurally.
"Everyone schedules for midnight UTC. How do you not melt the handlers at 00:00:00?"
Deterministic jitter per job_id spreads "approximately on the hour" jobs across a few second-buckets, the look-ahead window pre-loads the queue so there is no cliff, and per-tenant token buckets at the dispatcher cap any one customer's burst. Jobs that truly need exact timing opt out of jitter into a reserved-rate lane.
"The scheduler decides a job is due, then crashes. Lost fire or duplicate?"
Neither, by construction. The scheduler advances handoff_ts and appends an outbox row in one transaction, and a relay pushes to the queue and marks the row done only after a confirmed enqueue. A crash just replays at-least-once, and the dispatcher's lease CAS drops any duplicate. Durability-first forbids the lost fire, so I pay for the outbox.
"The whole fleet was down for 10 minutes. On recovery, what about all the missed runs?"
Policy, per job. My default for recurring jobs is catch-up-at-most-one: fire the most recent missed slot and skip the backlog, because replaying 10 minutes of hourly jobs is almost never what the user wants. It is configurable, and one-shots that are overdue just fire immediately.

What you can now explain in the room

  • Why durability outranks latency here, and what that buys when you have to choose.
  • Why the whole design exists to smear 230k fires in one tick, and the three levers that do it.
  • Why exactly-once execution is impossible over a network, and how fence plus stable key plus atomic transition get exactly-once effects instead.
  • Why sharding plus per-shard leader election beats one global scheduler, and what it costs for per-tenant fairness.
  • Why the scheduler needs an outbox to stop losing or duplicating the enqueue.
  • Where you spent the consistency budget (strong on the fire path, eventual on reads) and why that split is safe.

If you want to see how a real company frames the coordination and durability pieces, check our walkthrough on building the message queue that sits underneath systems like this.

Get a prep plan built around your Design a Distributed Job Scheduler interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your Design a Distributed Job Scheduler prep plan is on the way.