For a rate-limited logistics worker pool, give every job transition one owner: a durable queue releases normal retries, a cron sweep audits the ledger, and a dead-letter policy closes work that should stop. This is a governance decision before it is a timer decision. Two schedulers must never gain independent authority to drain the same failed jobs.

Short answer: queue eligible retries when minutes of polling delay would miss the recovery target, keep cron as a slow reconciliation mechanism, and send exhausted or non-retryable work to a dead-letter state that operators can inspect.

Write those responsibilities as an executable policy before selecting infrastructure. The policy must say who may make a job eligible, who may claim it, who may close it, and who may replay it. Only after those answers are testable should latency and cost decide whether normal eligibility comes from a delayed queue or an indexed scan.

Govern job ownership with an executable policy

Start in a notebook with transition rules, not a service comparison. A shipment job must never run before eligible_at; only the dispatcher may admit it; attempts must stop at the cap; and a permanent input failure must enter review without consuming every retry. These are small governance rules, but they survive a change of broker, database, or worker language.

The runnable model below treats admission as the central decision. It deliberately gives fresh and retried work the same bounded gate, then records terminal work separately. The numbers are test inputs, not a benchmark or a production claim.

from dataclasses import dataclass from enum import Enum class Outcome(Enum): READY = "ready" WAITING = "waiting" DEAD_LETTER = "dead_letter" @dataclass(frozen=True) class Job: job_id: str destination: str attempt: int eligible_at: int retryable: bool = True def classify(job: Job, now: int, max_attempts: int) -> Outcome: if not job.retryable or job.attempt >= max_attempts: return Outcome.DEAD_LETTER if job.eligible_at > now: return Outcome.WAITING return Outcome.READY def admit( jobs: list[Job], now: int, max_attempts: int, capacity: dict[str, int] ) -> tuple[list[Job], list[Job], list[Job]]: admitted: list[Job] = [] waiting: list[Job] = [] dead_letters: list[Job] = [] used = {destination: 0 for destination in capacity} for job in sorted(jobs, key=lambda item: (item.eligible_at, item.job_id)): outcome = classify(job, now, max_attempts) if outcome is Outcome.DEAD_LETTER: dead_letters.append(job) elif outcome is Outcome.WAITING: waiting.append(job) elif used.get(job.destination, 0) < capacity.get(job.destination, 0): admitted.append(job) used[job.destination] = used.get(job.destination, 0) + 1 else: waiting.append(job) return admitted, waiting, dead_letters jobs = [ Job("shipment-1042", "carrier-a", attempt=1, eligible_at=120), Job("label-8831", "carrier-a", attempt=2, eligible_at=118), Job("manifest-552", "carrier-a", attempt=3, eligible_at=110), Job("address-209", "carrier-b", attempt=1, eligible_at=90, retryable=False), ] admitted, waiting, dead_letters = admit( jobs, now=120, max_attempts=3, capacity={"carrier-a": 1, "carrier-b": 1}, ) assert [job.job_id for job in admitted] == ["label-8831"] assert {job.job_id for job in waiting} == {"shipment-1042"} assert {job.job_id for job in dead_letters} == {"manifest-552", "address-209"} 

One assertion is intentionally more revealing than a throughput chart: although two jobs for carrier-a are eligible, only one crosses the gate. Change the capacity, clock, attempt cap, and ordering policy in a parameterized eval harness. Add cases at exactly the eligibility boundary and exactly the attempt limit. For an AI-assisted document classification step, also persist the prompt version and token accounting with each attempt, because a scheduler retry must not erase the reason model spend increased.

I don't trust a notebook result as production proof by itself. It is the executable policy that should follow the code into CI, while integration tests add leases, acknowledgements, worker termination, and the external idempotency boundary. Fast feedback first. Reality next.

Retry and failure rules for SaaS jobs, queues, cron, and dead letters

Give each mechanism one state transition. The queue is the normal eligibility owner when an item should become runnable near its individual due time. Cron is the auditor: it finds durable records whose lease expired or whose expected enqueue marker is absent, then submits them through the same admission path. The dead-letter store owns attempts that reached the cap or were classified as non-retryable.

Discovery is not authorization. A queue delivery and a cron scan may both notice work, but only an atomic claim against the durable ledger may authorize execution. The envelope can stay small and language-neutral: job_id, kind, destination, attempt, eligible_at, and a reference to the payload in the system of record. A Node.js consumer and a Python policy harness can then share behavior without embedding broker details in shipment logic.

Dead letter should be a terminal state, not a shadow retry queue with no owner. Preserve the original job identifier, payload reference, attempt history, failure category, timestamps, and policy version. An invalid address can go directly to review; a temporary capacity response can use bounded backoff. Classification comes before delay calculation.

Replay is another owned transition. An operator records a reason, the system creates a replay attempt linked to the original history, and the job returns through normal admission. That makes replay subject to the same destination budget as fresh and automatically retried work. It also keeps an urgent manual action from becoming an invisible bypass around rate limits.

Then test the awkward boundary — external success followed by worker loss before acknowledgement. Redelivery is a normal possibility in an acknowledgement-based consumer protocol, so the job needs a stable idempotency key and the business operation needs a way to observe an existing result. RabbitMQ's documentation separates consumer delivery acknowledgements from publisher confirms; they protect different communication directions and should not be treated as one end-to-end transaction.

Rollout one workload without creating dual authority

Start with one workload class, such as warehouse notifications. Keep the existing dispatcher authoritative while the candidate path only computes eligibility decisions. Compare both paths against the same fixture: no early release, no attempt beyond the cap, no admission beyond the destination budget, and exactly one terminal transition for permanent work.

During cutover, assign each stable job identifier to exactly one dispatcher. A dual write that lets both cron and a queue execute the same identifier is not a gradual migration; it creates two authorities. Shadow evaluation is safe because it records a decision without claiming a job. Promotion transfers the authority flag only after the eval harness agrees.

Deployment needs the same clarity. Pause new admissions, allow bounded leases to finish, replace workers, and resume from the durable ledger. Rehearse worker loss after the external action but before acknowledgement, then verify that the stable idempotency key observes the existing result rather than repeating the business effect. Roll back by transferring dispatch authority, not by asking two mechanisms to race.

For prompt-bearing work, keep the prompt or policy version and token accounting with the attempt result. A replay after a prompt change is then distinguishable from another attempt under the old policy. This notebook-to-production trail makes quality regressions and repeated model spend visible during migration.

How can we implement one capacity gate for every eligible job?

A timer answers when a job becomes eligible. It cannot guarantee that the downstream system has room for it. Put a destination-specific admission layer between eligibility and execution, and make both fresh work and retries pass through it. That prevents a recovered batch from taking every slot while new carrier-label requests age behind it.

Consider a test fixture, not a claimed incident. At 09:00, 800 warehouse updates enter a pool configured to start 20 calls per second. At 09:00:02, 120 attempts receive retry times spread across the next two minutes after capacity responses. If those attempts mature on the same timestamp, a delayed queue can deliver a burst; if a cron sweep selects every due row, the database path can create the same burst. The admission budget protects the carrier boundary in either case. Select only the amount that can start, claim each record atomically, and leave the rest eligible. Then add fresh label requests to the fixture while retries mature and assert that the chosen fairness rule lets both classes advance. Jitter helps spread eligibility, but it isn't capacity control. Nor is queue priority a free substitute for fairness. RabbitMQ documents that priority queues have CPU and memory costs and recommends a small range of priority values. A simpler policy for many logistics flows is a small number of workload classes plus a fixed destination budget, with the ordering rule recorded as data rather than hidden in worker code. Kill one test worker after the carrier action but before acknowledgement, allow the delivery to return, and verify that its stable idempotency key observes the existing result. Finally, inject a permanent address failure and confirm that it reaches the terminal ledger once instead of competing for another slot. This single trace checks burst control, fairness, redelivery, and classification together; an average jobs-per-second number cannot do that.

Terminal means visible.

Let latency and operating cost choose the normal path

Once ownership, migration, and admission are settled, compare mechanisms against the workload rather than a price card:

Workload condition Primary mechanism Why
Recovery deadline is shorter than an acceptable scan interval Delayed queue release Each job can become visible near its own eligibility time
Durable jobs table is already authoritative and delay is relaxed Cron sweep A bounded indexed scan may be the smaller operational surface
A scheduler may miss or strand durable state Slow reconciliation sweep The ledger can be checked without creating a second execution policy
Failure is permanent or attempts are exhausted Dead-letter state The worker pool stops spending capacity on work that needs review

The catch is operational ownership. A queue is not suitable when its delivery state, retention, and consumer operations cost more engineering effort than the recovery target warrants; stick with an indexed cron scan when the full scan interval fits the service objective. Cron is not suitable when polling delay violates that objective or repeated scans have become material load. I'm not sure where that boundary falls without the due-row distribution and the business deadline. Those two measurements resolve it, and your mileage may vary by destination.

A tighter recovery target may justify precise delayed release, while a looser target may tolerate periodic discovery of due rows. Count queue operations, database reads, index maintenance, payload retention, worker wakeups, repeated carrier or model calls, and operator review. An expensive repeated model call can dominate the scheduler expense, so idempotency and attempt policy belong in the cost calculation.

Operate from state transitions. Monitor eligible backlog age by destination, admitted work, active leases, attempts by failure category, dead-letter arrivals, and replay outcomes. Backlog count alone is weak: 50 jobs can be harmless under one deadline and late under another. Alert on the oldest eligible job relative to its service objective, then correlate that age with pool saturation and downstream capacity responses.

The operational checklist is short enough to say in prose. Verify the due-time index, atomic claim, lease expiry, idempotency key, attempt cap, failure classifier, destination budget, dead-letter owner, replay audit trail, and alerts before enabling a workload class. Finally, drain a synthetic burst containing fresh work, retryable failures, and one permanent failure; the test passes only if capacity stays bounded, fresh work still advances under the chosen fairness rule, and the permanent item becomes inspectable exactly once.

Queues, cron, and dead letters are implementation tools. The durable ledger and admission invariants are the design.

References

Further reading