Skip to content
ZERONE
Natrag na uvide
Distribuirani sustavi2026-07-06 · 7 min čitanja

If you start Redis only for the job trigger, throw it back out: Postgres pg_notify as a worker queue

Our cinema-orch worker had to see new render jobs immediately — 5-second polling was too slow and pushed Postgres load into five-digit QPS. Adding Redis was the obvious reflex. We took \`LISTEN/NOTIFY\` instead, dropped latency below 200 ms and kept a whole component out of the stack.

The worker in our cinema-orch (Sprint 3.2) has one clear job: as soon as a user submits a render request through the Next.js app and the estimate check passes, the worker has to pick the job from the `jobs` table, spawn the pod and start the frame loop. Latency target: under one second from "user clicks render" to "pod provisioned".

The naive answer is polling — every 5 seconds `SELECT ... WHERE state = 'queued' ORDER BY created_at LIMIT 1`. Works, but two problems: median latency ~2.5 seconds (half the polling interval), and Postgres load per worker × polling frequency × concurrent workers reaches 4 QPS baseline on 20 workers at 5-second interval. On a Hetzner CX22 with a 2-vCPU Postgres, that's half the IO budget.

The German SaaS reflex is: add Redis for Pub/Sub. We didn't.

Postgres has built-in worker queues

Postgres has `LISTEN` and `NOTIFY` since 7.4 (2003). One client subscribes to a channel, another sends a signal, Postgres delivers to all subscribers. Payload size is 8 KB by default.

Our worker in Python with asyncpg listens for `new_render_job`, and the Next.js side executes `INSERT + NOTIFY` in the same transaction. If not in one transaction, the worker can receive the notify before the insert commits.

Three trade-offs to know

1. Notify delivery is at-most-once. Postgres does not persist notifies. If the worker isn't listening (restart, network partition), the signal is lost. Fix: after every reconnect, do a full scan of `jobs` for `state = 'queued'` before waiting on new notifies.

2. Payload size is 8 KB. Don't send full job objects; send only the channel and pick details from the table.

3. Notify is broadcast, not pop. All listening workers see the signal. Use `SELECT ... FOR UPDATE SKIP LOCKED` on pick to ensure only one takes it.

The numbers after the rewrite

  • Median latency "user clicks" → "pod spawns": 5.2 s → 620 ms.
  • Postgres baseline load without user traffic: 4 QPS → 0.1 QPS.
  • Zero Redis in the stack. One fewer container, backup target, monitoring category.

When Redis is still right

Delayed jobs, exponential backoff retries, weighted round-robin priority queues. LISTEN/NOTIFY doesn't do delayed. Above 1000 notifies/second, Postgres notify overhead becomes visible. We're at < 10/minute — no reason for a second storage system.

Meta-lesson

The "add Redis" reflex is from an era when Postgres was just a database. Post-2015 Postgres is a message bus + KV store + search engine + JSON store + time series in one — good enough for 90 % of use cases. If you don't have a specific requirement Postgres can't handle, running one server is cheaper and simpler than two.

The complete setup is live on ZER0ONE Lab Cinema Render and has run four weeks in beta without a single missed job.

Sličan izazov i kod vas?

Vjerojatno smo već vidjeli nešto slično. Razgovarajmo.

Započnite razgovor