Ako Redis startaš samo za job-trigger, izbaci ga natrag: Postgres pg_notify kao worker queue
Naš cinema-orch-worker morao je odmah vidjeti nove render jobove — 5-sekundno polling bilo je presporo i guralo je Postgres-load u peteroznamenkasti QPS-raspon. Redis dodati bio je očit refleks. Umjesto toga uzeli smo \`LISTEN/NOTIFY\`, spustili latenciju ispod 200 ms i cijelu komponentu držali izvan stacka.
Worker u našem cinema-orchu (Sprint 3.2) ima vrlo jasan posao: čim korisnik pošalje novi render request kroz Next.js aplikaciju i estimate check prođe, worker mora izvući job iz `jobs` tablice, spawnati pod i pokrenuti frame loop. Cilj latencije: ispod 1 sekunde od „user klikne Render" do „pod je provisioniran".
Naivno rješenje je polling — svakih 5 sekundi `SELECT ... WHERE state = 'queued' ORDER BY created_at LIMIT 1`. Funkcionira, ali ima dva problema: latencija je u medianu 2,5 sekunde (pola polling intervala), a Postgres-load po workeru × frekvencija pollinga × concurrent-workeri kod 20 workera i 5-sekundnog intervala postaje 4 QPS baseline-loada. Dodaš još kategorija workera (render-worker, upload-worker, cleanup-worker) i to brzo postane 15–30 QPS bez pravog user-prometa. Na Hetzner CX22 s 2-vCPU Postgresom to je polovica IO-budgeta.
Refleks u njemačkom SaaS-ekosustavu tada je: dodati Redis. Redis Pub/Sub za worker-notifikacije, Postgres samo kao persistence layer. Nismo to napravili.
Postgres iz kutije zna worker-queues
Postgres ima `LISTEN` i `NOTIFY` od verzije 7.4 (2003). Te dvije naredbe rade točno ono zbog čega se inače pokreće Redis Pub/Sub: jedan klijent se subscribea na kanal, drugi klijent šalje signal kanalu, a Postgres isporučuje signal svim subscriberima. Payload-size je 8 KB (kod standardne Postgres konfiguracije).
Cinema-orch-worker u Pythonu s asyncpg-om radi:
```python async def wait_for_next_job(conn): await conn.execute("LISTEN new_render_job") while True: # blokira dok ne stigne NOTIFY await conn.connection.notifies.get() job = await pick_next_queued_job(conn) if job: return job ```
Na Next.js app strani, izravno nakon uspješnog Inserta novog joba:
```typescript await db.execute(sql` INSERT INTO jobs (user_id, state, ...) VALUES (${userId}, 'queued', ...); NOTIFY new_render_job; `); ```
Dvije napomene o SQL-u: `NOTIFY` i `INSERT` moraju biti u istoj transakciji, inače worker može primiti Notify prije nego što je Insert commit-an. I payload može opcionalno sadržavati payload-string (`NOTIFY new_render_job, 'job_id=42'`), ali mi šaljemo samo kanal — worker sam dohvaća job s `SELECT ... FOR UPDATE SKIP LOCKED`.
Tri trade-offa koja treba poznavati
1. Notify-delivery je at-most-once. Postgres Notify ne sprema perzistentno. Ako worker upravo ne slušanja (restart, mrežna particija), propušta signal. Fix: worker nakon reconnecta uvijek prvo napravi full-scan `jobs` tablice za `state = 'queued'` rowove prije nego što čeka nove Notifyje. Tako propušteni Notify nije bitan.
2. Payload-size je 8 KB. Ako želiš slati cijele job-objekte preko Notifyja, ne ide. Mi ionako šaljemo samo kanal — worker detalje dohvaća iz tablice.
3. Notify je broadcast, ne pop. Svi workeri koji slušanjaju isti kanal vide signal. Ako želiš osigurati da samo jedan uzme job, trebaš `SELECT ... FOR UPDATE SKIP LOCKED` pri Picku — Postgres lockira row na DB-razini, drugi workeri je pri sljedećem `SELECT`-u vide kao „skipped".
Naš cinema-orch ima jedan worker po server-instanci, zato `SKIP LOCKED` kod nas nije ni potreban — ali to je standardna preporuka ako se ikad horizontalno skalira.
Brojke nakon prepravke
- Median-latencija „user klikne" do „pod spawna": 5,2 s → 620 ms. Najveći dio sada je 400 ms Runpod-pod-provisioninga, više ne polling-wait-time.
- Postgres-load baseline bez pravog user-prometa: 4 QPS → 0,1 QPS (samo heartbeat-tickovi).
- Nema Redisa u stacku. Jedan Docker container manje, jedan backup-target manje, jedna kategorija procesa manje u monitoringu.
Kada Redis ipak ima smisla
Ako trebaš Delayed Jobs (`SCHEDULE FOR '2026-08-15T09:00:00Z'`), zakašnjeli retry s eksponencijalnom backoff-krivuljom, ili kompleksne priority-queues s Weighted-Round-Robinom. `LISTEN/NOTIFY` ti ne daje delayed-jobs. Za instant-jobove s jednostavnom FIFO-semantikom Postgres je dovoljan.
Ako trebaš > 1000 Notifyja u sekundi — i tada. Postgres NOTIFY ima overhead po Notify-emitu, od 1k/s to postaje vidljivo. Kod nas je trenutačno < 10 Notifyja po minuti. Nema razloga za pogon drugog storage-sustava.
Meta-lekcija
Njemački SaaS-refleks „dodaj Redis" dolazi iz doba kad je Postgres uistinu bio samo baza. Post-2015 Postgres je message-bus + KV-store + search-engine + JSON-store + time-series u jednom — i sve te značajke dovoljno su dobre za 90 % use-caseova. Ako nemaš specifičan zahtjev koji Postgres uistinu ne može pokriti, jeftinije je i jednostavnije pogoniti jedan server nego istovremeno održavati dva.
Kompletan setup je live na ZER0ONE Lab Cinema Render i radi već 4 tjedna u beti bez ijednog propuštenog joba. Ako za vlastiti backend evaluiraš `LISTEN/NOTIFY`-baziranu job-queue i imaš pitanja — javi se na [email protected], rado ćemo 30 minuta besplatno raspraviti setup.