Skip to content

Commit 63da117

Browse files
NikolaySNik Samokhvalovclaude
authored
docs: three-latencies explanation (#68)
* docs: three-latencies explanation (producer / subscriber / end-to-end) Name the three distinct latencies in any Postgres queue and explain why PgQue's batch-ticker model makes #1 and #2 sub-ms while bounding #3 by the tick cadence (not by load). Addresses recurring confusion about the apparent contradiction between sub-ms consumer-path latency and the ~1 s end-to-end delivery bound. - README.md: brief paragraph + 3-bullet list, new "Three latencies" subsection between "Latency trade-off" and "Comparison". - docs/pgq-concepts.md: detailed version with per-latency physics, tick-frequency trade-off table, comparison to pgmq's poll-on-demand model, when-to-pick guidance, provenance link. Uses actual pgque.sql column names: queue_ticker_max_lag (3s), queue_ticker_idle_period (1min idle-decelerator), queue_ticker_max_count (500). The 1-second cadence comes from the pg_cron schedule set by pgque.start(), not from queue_ticker_idle_period. * docs: promote three-latencies to dedicated page Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: fix three-latencies asymmetry Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: soften unimplemented metadata-rotation claim Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(three-latencies): drop forward-reference to unmerged work Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(three-latencies): drop positioning + provenance Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Nik Samokhvalov <nik@Niks-MacBook-Pro.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d96817b commit 63da117

4 files changed

Lines changed: 58 additions & 0 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Discussion on [Hacker News](https://news.ycombinator.com/item?id=47817349).
2121

2222
- [Why PgQue](#why-pgque)
2323
- [Latency trade-off](#latency-trade-off)
24+
- [Three latencies](#three-latencies)
2425
- [Comparison](#comparison)
2526
- [Installation](#installation)
2627
- [Roles and grants](#roles-and-grants)
@@ -70,6 +71,16 @@ Ways to reduce delivery latency: tune tick frequency and queue thresholds; use `
7071

7172
If your top priority is single-digit-millisecond dispatch, PgQue is the wrong tool. If your priority is **stability under load without bloat**, that is where PgQue fits.
7273

74+
## Three latencies
75+
76+
"Queue latency" is three numbers, not one:
77+
78+
1. **Producer latency**`send` / `insert_event`. Sub-ms.
79+
2. **Subscriber latency**`next_batch` over a pre-built batch. Sub-ms.
80+
3. **End-to-end delivery**`send` → consumer visibility. ≈ tick period. Tunable, not floored. Does not grow with load.
81+
82+
See [docs/three-latencies.md](docs/three-latencies.md) for the breakdown, tick-cadence trade-off table, and comparison with UPDATE/DELETE-based designs.
83+
7384
## Comparison
7485

7586
| Feature | PgQue | PgQ | PGMQ | River | Que | pg-boss |

docs/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ Short docs for users, plus a contributor primer.
1212
batch send, recurring jobs, DLQ inspection.
1313
- **[Benchmarks](benchmarks.md)** — current throughput numbers and
1414
methodology.
15+
- **[Three latencies](three-latencies.md)** — producer latency, subscriber
16+
latency, and end-to-end delivery explained; tick-cadence trade-off table;
17+
comparison with UPDATE/DELETE-based designs.
1518
- **[PgQ concepts](pgq-concepts.md)** — glossary of the core vocabulary
1619
(event, batch, tick, rotation, ticker rule). Useful alongside the
1720
tutorial.

docs/pgq-concepts.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,9 @@ below; the function auto-prefixes `queue_` internally.
6666
> produce huge batches consumers can't handle.
6767
6868
— Kreen & Pihlak, PgCon 2009
69+
70+
## Three latencies
71+
72+
For the full explanation — producer latency, subscriber latency,
73+
end-to-end delivery, tick-cadence trade-offs, and comparison with
74+
UPDATE/DELETE-based designs — see [three-latencies.md](three-latencies.md).

docs/three-latencies.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Three latencies
2+
3+
"Queue latency" is three numbers, not one. Conflating them confuses design discussion — each reflects a different bottleneck, and PgQue's trade-offs only make sense once they are separated.
4+
5+
| # | Name | What it is | PgQue | Bottleneck |
6+
|---|---|---|---|---|
7+
| 1 | Producer | `send` / `insert_event` → durable | sub-ms (~high-µs; ~86k ev/s PL/pgSQL single-INSERT in prelim bench) | WAL flush, triggers |
8+
| 2 | Subscriber | `next_batch` + `get_batch_events` returning an already-built batch | sub-ms (snapshot SELECT, no SKIP LOCKED scan; ~2.4M ev/s consumer read) | how "next work" is located |
9+
| 3 | End-to-end | `send` → consumer visibility | ≈ tick period + consumer poll interval | ticker cadence (tunable) |
10+
11+
#3 is what application behavior depends on (SLAs, retries, perceived staleness). The trap: #3 is bounded below by #1 + #2, but **the magnitude of #1 and #2 doesn't determine #3** — tick cadence and consumer poll interval do. You can drive #1 and #2 to microseconds and still have #3 in seconds because the ticker hasn't fired yet. The reverse — sub-ms #3 while #1 or #2 takes seconds — isn't possible: a message can't be visible to a consumer faster than it can be written and read.
12+
13+
## End-to-end is tunable, not floored
14+
15+
The default 1-second tick is a `pg_cron` schedule, not a design floor. PgQue's e2e is bounded by whatever tick cadence you configure. Sub-ms e2e is achievable with more aggressive ticking:
16+
17+
- **Staggered `pg_cron` jobs.** Schedule N jobs at `1 second` each, offset by `1/N` via a shared coordinating lock, to get effective tick periods down to ~10 ms (N=100) or ~1 ms (N=1000).
18+
- **In-tick sleep loop.** Single cron callout that internally does `pg_sleep(0.01)` ×100 inside one invocation — same effective cadence, fewer scheduler wakeups.
19+
- **Native sub-second cron.** `pg_cron` does not yet support sub-second schedules natively; the staggered-job or in-tick-sleep workarounds are the current approach.
20+
21+
Trade-off at very high tick rates: every tick UPDATEs `pgque.tick` and `pgque.subscription`, so more ticks = more dead tuples on those metadata tables under held-xmin conditions. The event tables stay bloat-free (TRUNCATE rotation); the metadata-table bloat is a separate story. PgQue v0.2.0 has the same UPDATE pattern on the small subscription/tick tables as upstream PgQ — the bloat shape is bounded but real under sustained held-xmin.
22+
23+
Rough guidance:
24+
25+
| `pg_cron` schedule | Average e2e | Notes |
26+
|---|---|---|
27+
| `1 second` (default) | ~500 ms | pgqd-compatible, minimal metadata churn |
28+
| `250 ms` | ~125 ms | 4× metadata writes, still cheap |
29+
| `10 ms` staggered | ~5 ms | needs coordinated jobs or in-tick sleep |
30+
| `1 ms` staggered | sub-ms | kHz-range; metadata-table rotation recommended |
31+
32+
Per-queue thresholds (`queue_ticker_max_lag` default `3 seconds`, `queue_ticker_max_count` default 500, `queue_ticker_idle_period` default `1 minute` idle-decelerator) go through `pgque.set_queue_config()`.
33+
34+
## Load behavior: PgQue vs. UPDATE/DELETE designs
35+
36+
The key property of the tick model: **e2e does not grow with load.** The ticker fires at its configured rate regardless of backlog, so under pressure batch size grows (up to `queue_ticker_max_count`) — not e2e.
37+
38+
UPDATE/DELETE-based systems use a different model: a consumer call returns messages immediately, marking them consumed via UPDATE (claim) and DELETE (ack) rather than advancing a snapshot cursor. So e2e ≈ consumer poll interval — sub-ms when the consumer is actively polling, up to the poll interval otherwise. Drain rate is `batch_size / poll_interval`; if producers outrun that, queue depth grows and e2e grows with it until consumers scale out. Separately, those UPDATEs and DELETEs produce dead tuples that autovacuum cannot reclaim under MVCC pressure (long-running tx, idle-in-transaction, lagging logical replication slot, physical standby with `hot_standby_feedback=on`) — the bloat failure mode [PgQue avoids by construction](../README.md#why-pgque).

0 commit comments

Comments
 (0)