CREATE TYPE outbox_status AS ENUM ('pending', 'retry', 'processing', 'done', 'dead');
CREATE TABLE outbox_events (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
dedupe_key TEXT NOT NULL,
topic TEXT NOT NULL,
payload JSONB NOT NULL,
status outbox_status NOT NULL DEFAULT 'pending',
attempts INT NOT NULL DEFAULT 0,
max_attempts INT NOT NULL DEFAULT 5,
available_at TIMESTAMPTZ NOT NULL DEFAULT now(),
locked_by TEXT,
locked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- producers stay idempotent: a repeated enqueue is a no-op
CREATE UNIQUE INDEX idx_outbox_dedupe ON outbox_events (dedupe_key);
-- partial index so the claim query only touches unfinished work
CREATE INDEX idx_outbox_claimable
ON outbox_events (available_at)
WHERE status IN ('pending', 'retry');
INSERT INTO outbox_events (dedupe_key, topic, payload)
VALUES ('order-42-created', 'orders.created', '{"order_id": 42}')
ON CONFLICT (dedupe_key) DO NOTHING;
WITH claimed AS (
SELECT id
FROM outbox_events
WHERE status IN ('pending', 'retry')
AND available_at <= now()
ORDER BY available_at
LIMIT :batch_size
FOR UPDATE SKIP LOCKED
)
UPDATE outbox_events AS e
SET status = 'processing',
attempts = e.attempts + 1,
locked_by = :worker_id,
locked_at = now()
FROM claimed
WHERE e.id = claimed.id
RETURNING e.id, e.topic, e.payload, e.attempts, e.max_attempts;
import json
import time
import psycopg2
import psycopg2.extras
CLAIM_SQL = open("claim_events.sql").read()
class OutboxWorker:
def __init__(self, dsn, worker_id, batch_size=20):
self.conn = psycopg2.connect(dsn)
self.conn.autocommit = True
self.worker_id = worker_id
self.batch_size = batch_size
def claim_batch(self):
with self.conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute(CLAIM_SQL, {
"batch_size": self.batch_size,
"worker_id": self.worker_id,
})
return cur.fetchall()
def mark_processed(self, event_id):
with self.conn.cursor() as cur:
cur.execute(
"UPDATE outbox_events SET status = 'done', locked_by = NULL, "
"locked_at = NULL WHERE id = %s",
(event_id,),
)
def mark_failed(self, event_id, attempts, max_attempts):
# exhausted retries land in the dead state; otherwise back off
if attempts >= max_attempts:
new_status, delay = "dead", 0
else:
new_status, delay = "retry", min(60 * attempts, 900)
with self.conn.cursor() as cur:
cur.execute(
"UPDATE outbox_events SET status = %s, locked_by = NULL, "
"locked_at = NULL, available_at = now() + make_interval(secs => %s) "
"WHERE id = %s",
(new_status, delay, event_id),
)
def run(self, publish):
while True:
rows = self.claim_batch()
if not rows:
time.sleep(1.0)
continue
for row in rows:
try:
publish(row["topic"], row["payload"])
self.mark_processed(row["id"])
except Exception:
self.mark_failed(row["id"], row["attempts"], row["max_attempts"])
A common failure mode in worker queues is the read-then-update race: one query selects pending rows, another updates them, and between the two calls a second worker grabs the same rows. This snippet shows the PostgreSQL idiom that closes that gap by folding the claim and the state transition into a single statement using UPDATE ... RETURNING combined with FOR UPDATE SKIP LOCKED.
The schema.sql tab defines the outbox_events table as a durable queue. Each row carries a status, an attempts counter, a locked_by worker id, and a locked_at timestamp. The partial index idx_outbox_claimable is the performance heart of the design: it indexes only rows in the pending or retry state and orders them by available_at, so the claim query never scans already-processed history.
The claim_events.sql tab is where atomicity happens. The CTE claimed selects up to :batch_size claimable rows, and crucially applies FOR UPDATE SKIP LOCKED inside the subquery. FOR UPDATE row-locks the candidates; SKIP LOCKED tells Postgres to silently ignore rows another transaction already locked instead of blocking on them. The outer UPDATE then flips those exact rows to processing, stamps locked_by/locked_at, bumps attempts, and RETURNING hands the full claimed payloads back to the caller. Because select-and-mark is one statement in one implicit transaction, no two workers can ever claim the same event.
The worker.py tab shows the consumer loop. claim_batch runs the claim query and returns rows already marked processing, so the worker owns them the moment it sees them. On success mark_processed transitions to done; on failure mark_failed either reschedules with a backoff via available_at or moves the row to dead once max_attempts is exceeded. The dedupe_key column and ON CONFLICT in the schema make producers idempotent, guarding against duplicate enqueues.
The trade-off is that claimed-but-crashed rows stay in processing until a reaper resets stale locks by locked_at, so at-least-once, not exactly-once, delivery is the realistic guarantee. This pattern scales horizontally without an external broker and is the standard way to build a reliable outbox on plain Postgres.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
Share this code
Here's the card — post it anywhere.