How much slower will our queries be?
- Date
- By
- CipherStash Team
On the fast paths, barely. Exact match and OPE range queries run at about 0.12 ms, within 1.2 to 1.4 times the same query on plaintext, and stay flat from 10 thousand to 10 million rows. Free-text match is about 16 ms at a million rows. Decrypting the results adds one ZeroKMS request per page: a 100-row read with three encrypted fields takes about 43 ms end to end in the same AWS region. Writes cost client CPU, where encryption happens in your process.
These are our numbers on our machine. An Apple M1 Max running PostgreSQL 17 and EQL v3, single connection, medians across datasets from ten thousand to ten million rows. Every figure is the encrypted query against the same query on a plaintext table with an equivalent index, so the ratio is the honest part and the milliseconds are the part that will differ on your hardware.
At one million rows
| Operation | Encrypted median | Against plaintext |
|---|---|---|
| Exact match | 0.12 ms | 1.3x |
| Range and ORDER BY (OPE) | 0.12 ms | 1.2x |
| Range and ORDER BY (ORE) | 0.52 ms | 5.2x |
| JSON containment | 0.40 ms | 1.4x |
| GROUP BY, low cardinality | 83 ms | 2.2x |
| Free-text match | 15.7 ms | n/a (100 to 400x faster than a sequential scan) |
Exact match and OPE range stay at 0.12 ms from ten thousand rows to ten million. Latency on an indexed encrypted column tracks the index, not the table.
The slow paths, named
ORE ordering costs about five times plaintext and its index takes about 44 seconds to build at a million rows, against about one second for OPE. OPE is the default. ORE is a per-column choice you make for its leakage profile, not its speed.
GROUP BY scans every matched row and grows with the group, about twice plaintext throughout. Free-text match is the slowest read at about 16 ms, and the figure that matters there is that the bloom index engages at all. Without it you are scanning.
Decryption and the ZeroKMS round trip
The figures above are query time inside Postgres. Reading results back also means decrypting them, and decryption needs key material from ZeroKMS: one network request per batch, the same as any KMS needs a request. The SDK's bulk decrypt covers a whole page of rows in a single request, so the cost is per page rather than per value.
The kms-app benchmark measures that end to end. A Next.js app with Postgres reads a page of records with three encrypted fields each and bulk-decrypts them. It runs on AWS in the same region as ZeroKMS, with the load generator on a separate instance. Median across three rounds, for the whole request including the database query:
| Rows read | Values decrypted | p50 | p95 |
|---|---|---|---|
| 20 | 60 | 15 ms | 32 ms |
| 100 | 300 | 43 ms | 60 ms |
AWS KMS has no bulk decrypt, so the same 100-row read through it made 300 separate calls and took about 314 ms at p50 and 773 ms at p95. Keep ZeroKMS in the same region as your application, and decrypt a page at a time rather than a row at a time.
Writes
Encryption runs in your application on every write. On the reference machine that is about 11,000 rows a second for exact-match and OPE columns and about 1,300 rows a second for the free-text domain, which builds a bloom filter and an order term per value. Those are single-threaded figures. Batching and parallel writers scale past them.
Check it yourself
git clone https://github.com/cipherstash/benchesThe repo has the per-scenario SQL, the EXPLAIN plans and a comparison against the previous EQL version. Run it on the hardware you deploy to before you quote a number to anyone.