Limits
What pg_crdt 0.0.1 does not do, and the numbers that bound what it does. The measurements are single runs on a laptop, in the benchmark reports for the PN-Counter, for text and for vacuum, bloat and WAL: they show how the cost grows, not what you will get.
Numbers
Section titled “Numbers”| Limit | Value | What happens past it |
|---|---|---|
A count (an entry of p or n) |
0 to 9,223,372,036,854,775,807, a whole number | pn_increment and pn_decrement raise 22003. pn_apply_delta rejects the delta with 22023. |
The value, sum(p) - sum(n) |
The bigint range |
pn_value raises 22003. |
An amount for pn_increment or pn_decrement |
0 to 9,223,372,036,854,775,807 | A negative amount raises 22023. |
| An entry key | A replica id, or <replica id>:<device>: non-empty, the replica id without : |
22023 from pn_increment, pn_decrement and pn_apply_delta. pn_merge accepts any key. |
| A collaborative text document id | text, non-empty, at most 256 bytes, no control characters |
22023 from ydoc_create. An id that exists raises 23505. |
| Format version | v = 1 |
A crdt.pn or crdt.yseq state with another v fails the domain’s check with 23514. |
Database encoding for yseq_ |
UTF8 | In another encoding, yseq_ops_since, yseq_diff and yseq_merge fail (54000) on a state with deleted characters. |
| Postgres versions | 16, 17, 18 | Not tested on others. |
Collaborative text has its own limits (key length, quotas of the untrusted path; for ydoc_ the document id and row size): see the column functions and the table-backed functions.
Behavior
Section titled “Behavior”- Two types. The PN-Counter and collaborative text are built; collaborative text has an experimental column form (
yseq_) and a table-backed form (ydoc_). Add-wins sets, last-writer-wins registers and maps are planned: see the overview. - The state grows with the number of replicas. An entry is added for every replica id that writes, and entries are never removed. In the benchmarks a state with 10 replicas was 496 bytes, 100 replicas 1.3 kB, 1,000 replicas 12 kB and 10,000 replicas 120 kB (TOAST-compressed at about 9 bytes per entry from 1,000 replicas up).
- Cost grows with the state. Merging a one-entry delta into a state took about 11 microseconds at 10 replicas and about 1.2 ms at 10,000. Merging two 10,000-replica states took about 14 ms.
pn_is_validtook about 4 ms at 10,000 replicas, which is why the domain checks the envelope only. - So does the WAL of a state column. Up to about 100 replicas (1 kB) the state stays in the row, and an update logs only the bytes that changed: 128 bytes of WAL per write at 10 replicas, less than inserting a delta row (about 350 bytes). Above about 2 kB the state is TOASTed, and every write rewrites and logs all of it: about 10 kB per write at 1,000 replicas and 92 kB at 10,000. For counters with many writers, insert deltas into a table and read with
pn_merge_agg. See Storage and performance. - Compaction of a delta table is not built. A table of deltas grows until you fold it into a snapshot yourself. Storage and performance shows how.
- Change feeds carry the whole state. Logical decoding sends the full new value of a changed column, and some feeds cap payloads (Supabase Realtime, for example, drops column values over 64 bytes from change payloads above 1 MB). Treat a change event as “this row changed” and fetch the row.
- No invariants. A counter cannot be kept above zero across disconnected writers. A
checkonpn_valuerejects a merge that breaks it, and does not reconcile it. - No reset, no removal of a writer. Totals only grow. See PN-Counter.
- No merge operator. There is no
||for merging: onjsonbit means concatenation. Usepn_merge. - A lost state is silent. A writer that restarts from 0 under its old key is ignored until it passes its old total.
pn_apply_deltacannot detect it. See Replicas and identity. - The default identity is only as strong as the settings it reads. See Trust model.
- No tombstone collection, no deltas by version vector, and no upgrade path (this is 0.0.1). See Versioning.
- Postgres 18 virtual generated columns cannot call user-defined functions yet. Use
stored. - The schema is fixed. The extension is not relocatable: it always installs into
crdt. - Dropping the extension drops columns that use
crdt.pnorcrdt.yseq(withcascade), and everyydoc_document. See Install.
Collaborative text
Section titled “Collaborative text”- The column (
yseq_) is experimental, and its cost grows with the document. Every edit rewrites the whole value: in the benchmarks a keystroke wrote about 3.4 kB of WAL at 1 kB of text, 7.1 kB at 5 kB, 18.5 kB at 20 kB and 78.7 kB at 100 kB, and each edit leaves a dead TOAST copy that onlyVACUUMreclaims, so a table edited at keystroke rate bloats to many times its live size.yseq_merge,yseq_applyandyseq_ops_sinceread the whole state on every call. Large documents are a known limitation: useydoc_for them. See Limits of the column. - The column never forgets. Deleted characters stay in a
crdt.yseqstate as runs (without their text); there is no tombstone collection. - No equivocation detection in the column.
yseq_applyandyseq_mergeskip a run whose ids are already present without comparing its text, so two writers sharing one entry key lose text silently. - A three-way save of the column sends the whole state as its base, and the
yseq_edits return the state, not the op (useyseq_diff). - Plain text only. No rich text, cursors, presence or undo, and the ops are not the Yjs wire format. Outside JavaScript, a live editor needs its own implementation of the algorithm; in JavaScript, use the JavaScript client. See What it cannot do.
- Deleted text is kept. Tombstones keep their characters for peers that have not seen them.
ydoc_compactmerges tombstone rows but does not remove them, and there is no garbage collection. - Storage of
ydoc_. In the benchmarks a 5 kB post took about 24 kB across the tables, 3.8 times theyseq_column.ydoc_valuereads every row of the document (about 0.4 ms at 10,000 characters, 3.6 ms at 100,000 on the benchmark machine). - No index on
ydoc_text.ydoc_valueisSTABLE, so it cannot be in a generated column or an expression index. Search a cached text column instead. - Compaction is a call you make. Nothing compacts documents by itself. Schedule
ydoc_compact. ydoc_ops_sinceandyseq_ops_sincesend every delete, not only the new ones, because deletes carry no clock.- One writer per entry key. Two writers that use the same key make ops that other
ydoc_copies refuse (22023), and lose text in ayseq_column. Give each editor and device its own key. - One client library, for text in JavaScript. The JavaScript client syncs
ydoc_documents through a transport you write; for ayseq_column your application callsyseq_applyandyseq_ops_sinceitself. The PN-Counter needs none: the examples use plain node-postgres, and any driver works, because the state isjsonb. See also the client’s limits.