Engineering Decisions
The Symptom
Spotting the Bug
The Move to a Queue
Ts
Idempotency at Enqueue, Not Just Send
If a candidate already has an active (queued or running) job for this sequence, reuse it instead of creating a new one.The check is one query against a partial index on (sequence_id, candidate_id) filtered to active states. It is cheap enough to do unconditionally on the enqueue path, and it makes the API safe to retry. A second layer covers the case where two requests pass the check simultaneously and both insert: a unique constraint on the same tuple, partial-indexed to active states, plus ON CONFLICT DO NOTHING. The user gets one job per candidate per sequence, however hard they click.
Per-Candidate Send Gates
Concurrency That Backs Off Gracefully
- Start at 3.
- On 429, drop to 1.
- After a successful run, ramp back up.
Credit Semantics Survive the Move
- Pre-flight check at enqueue: do we have the credits to attempt this batch?
- Deduction inside the worker, atomic with the unlock result.
- Refund on failed_no_email or failed_transient after retry budget is exhausted.
Where to Over-Engineer
The Status Endpoint Is the Contract
Ts
What Was Tempting and Wrong
Reusing the existing send queue for unlocks
Surfacing per-job progress as a websocket stream
The Shape That Generalizes
- Move the slow external call into a queue.
- Make enqueue idempotent against the natural key.
- Gate downstream actions per-item, not per-batch.
- Adjust concurrency on the response code, not the clock.
- Treat the credit ledger as append-only and refund forward.
- Poll for status; do not stream.