From 69e5d2baca12b49f2935f5957f8151403d781b6b Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Wed, 8 Jul 2026 19:49:41 +1000 Subject: [PATCH] docs(reference): add the benchmarks page Publishes the EQL v3 query-performance numbers from cipherstash/benches at /reference/benchmarks, so the performance claims have a docs page to land on: - encrypted-vs-plaintext query latency (exact match and OPE range within ~1.2-1.4x of plaintext, flat from 10k to 10M rows) - the OPE-vs-ORE range/order tradeoff, free-text bloom match, JSON, GROUP BY - ingest throughput, methodology, caveats, and repro steps Every figure is sourced from the public benches report and cross-checked against the plaintext baseline. Ticks the IA checklist item. --- IA.md | 2 +- content/docs/reference/benchmarks.mdx | 73 +++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 content/docs/reference/benchmarks.mdx diff --git a/IA.md b/IA.md index a56a5cc..f470152 100644 --- a/IA.md +++ b/IA.md @@ -434,7 +434,7 @@ and the old `/compare/*` paths redirect there (`v2-redirects.mjs`). - [ ] `/reference/proxy/*` (configuration, message-flow, multitenant, errors) - [ ] `/reference/workspace/billing` + `/members` + `/configuration` - **Cross-cutting:** -- [ ] `/reference/benchmarks` — listing numbers + methodology (CIP-3334) +- [x] `/reference/benchmarks` — listing numbers + methodology (CIP-3334) - [ ] `/reference/agent-skills` (port; expand per CIP-3339) - [ ] `/reference/glossary` (port) - [ ] Repoint `scripts/generate-docs.ts` TypeDoc output → `content/docs/reference/stack` diff --git a/content/docs/reference/benchmarks.mdx b/content/docs/reference/benchmarks.mdx new file mode 100644 index 0000000..91d5097 --- /dev/null +++ b/content/docs/reference/benchmarks.mdx @@ -0,0 +1,73 @@ +--- +title: Benchmarks +description: "Query latency on encrypted columns versus plaintext PostgreSQL. Exact match and range run within 1.2-1.4x of plaintext and stay flat from 10k to 10M rows." +type: reference +components: [eql] +--- + +CipherStash encrypts data in your application and queries it in PostgreSQL through [EQL](/reference/eql). These benchmarks measure what that costs: the same query shapes run against encrypted columns and against plaintext tables with equivalent indexes, so every figure is an honest encrypted-versus-plaintext ratio rather than a number in isolation. + +All results are medians on **Apple M1 Max, PostgreSQL 17, EQL v3**, across datasets from 10,000 to 10,000,000 rows. The full suite (per-scenario SQL, `EXPLAIN` plans, charts, and the v2-to-v3 regression comparison) lives in [cipherstash/benches](https://github.com/cipherstash/benches). + + +On the fast paths, querying encrypted data costs almost nothing. Exact match and OPE range queries run at about **0.12 ms, within 1.2-1.4x of plaintext PostgreSQL, and stay flat from 10k to 10M rows**. Latency tracks the index, not the row count. + + +## Query latency + +Median latency at 1,000,000 rows, with the ratio to the same query on a plaintext table with an equivalent index. + +| Operation | EQL index | Encrypted median | vs plaintext | +| --- | --- | --- | --- | +| Exact match (`=`) | B-tree (equality term) | 0.12 ms | 1.3x | +| Range / `ORDER BY` (OPE) | B-tree (OPE order term) | 0.12 ms | 1.2x | +| Range / `ORDER BY` (ORE) | B-tree (ORE order term) | 0.52 ms | 5.2x | +| JSON containment (`@>`) | GIN (SteVec) | 0.40 ms | 1.4x | +| `GROUP BY` (low cardinality) | B-tree (equality term) | 83 ms | 2.2x | + +**Range and ordering have two index options.** The `_ord_ope` variants use order-preserving encryption (OPE): near-plaintext latency, and an index that builds in about 1 s at 1M rows. The `_ord` variants use order-revealing encryption (ORE): roughly 5x plaintext, and the index takes about 44 s to build at 1M. They have different leakage profiles, so the right choice is per column rather than universal, see [Searchable encryption](/concepts/searchable-encryption) for the tradeoff and [Numbers](/reference/eql/numbers) for the variants. + +**Free-text match** uses a bloom-filter term (there is no `LIKE` on encrypted text) and runs at about 15.7 ms at 1M rows. What matters most is that the index engages at all: the bloom GIN is **100-400x faster than a sequential scan**. See [Text](/reference/eql/text). + +## Scaling + +Latency on an indexed encrypted column tracks the index, not the table, so the encrypted-versus-plaintext ratio barely moves as the dataset grows 1000x: + +| Operation | 10k | 100k | 1M | 10M | +| --- | --- | --- | --- | --- | +| Exact match (`=`) | 0.13 ms (1.4x) | 0.11 ms (1.2x) | 0.12 ms (1.3x) | 0.13 ms (1.4x) | +| Range (OPE) | 0.12 ms (1.2x) | 0.12 ms (1.2x) | 0.12 ms (1.2x) | 0.12 ms (1.2x) | + +Aggregations that scan every matched row, like `GROUP BY`, grow with the number of rows in the group exactly as they do on plaintext: about 2x plaintext throughout (2.1 ms at 10k, 83 ms at 1M). + +## Ingest + +Encryption happens in your application on write, so insert throughput reflects client-side work rather than database load. On the reference machine, encrypted insert throughput ranges from about **11k rows/s** for exact-match and OPE columns down to about **1.3k rows/s** for the free-text `text_search` domain, which generates a bloom and an order term per value. Batching and parallel writers scale this well past the single-threaded figures here. The per-domain breakdown is in the [ingest report](https://github.com/cipherstash/benches/blob/main/report/BENCHMARK_REPORT.md). + +## Methodology + +- **Environment:** Apple M1 Max, PostgreSQL 17, EQL v3. Datasets of 10k, 100k, 1M, and 10M rows. +- **Baseline:** every encrypted scenario is compared to the same query shape on a plaintext table with an equivalent index (`benches/plaintext_v3.rs`). Ratios are encrypted median divided by plaintext median. +- **Indexes:** queries use functional indexes over EQL's extractor terms, exactly as documented in [Indexes](/reference/eql/indexes). A query that cannot use its index falls back to a sequential scan, as it would on plaintext. +- **Caveats:** medians are single-machine and single-connection, so absolute numbers on your hardware and under concurrency will differ. One outlier is called out in the report: JSON `contains` at 10M rows uses a query recipe that materializes the full GIN bitmap before `LIMIT` (a documented plan artifact, not the operator's real cost). + +## Reproduce + +```bash +git clone https://github.com/cipherstash/benches +cd benches +mise run setup-db-v3 +mise run bench:v3:query:all 1000000 +mise run report:v3-compare +``` + +## Related + + + + The leakage model behind each index type: what exact, range, and match terms reveal. + + + The functional-index recipes these numbers depend on, and what it takes for an index to engage. + +