Performance
What one small server does under load, how it was measured, and how to measure your own.
These are the rates one default install sustained with zero errors at every figure. They are measured, not projected, and they are repeated before every release that touches the request path. Read the setup before reading the numbers: a different box, proxy or dataset gives different ones.
The numbers
Section titled “The numbers”| Workload | Sustained, zero errors |
|---|---|
| Record reads, realistic mix | 1,200+ req/s |
| Single-record fetches | 3,000+ req/s |
| Record writes | 1,600+ req/s |
| Mixed, 80 % reads / 20 % writes | 1,000 req/s |
“Realistic mix” is 40 % deep list pages (page 100 to 3,300 of 30 records), 20 % filtered lists, 15 % sorted lists and 25 % single-record fetches, every list returning 30 records of about 1 KB each. Writes are record creates on a second collection. The single-record figure is where the load generator gave out, not the server.
The setup
Section titled “The setup”- Box: a shared 4 vCPU, 8 GB VPS (Contabo), nothing tuned beyond the two settings in
the deploy notes:
nginx’s connection limit (see the header of
deploy/nginx/vaultbase.conf) and no page-cache dropping cron. - Server: Vaultbase 0.23.2 as shipped, read worker threads at their default, behind nginx with TLS and gzip from the deploy templates.
- Data: one collection of 100,000 records with the indexes the Monitoring page
suggested for the queries in the mix (
views,(status, created_at)), plus a second collection taking the writes. - Client: one machine on the public internet, about 85 ms of round trip away. Every latency a client sees includes that; the figures above are rates, which don’t. The write and mixed figures were repeated from a second machine in another provider’s datacenter, about 5 ms away, and topped out at the same step.
- Method: warm up at 50, 150 and 400 requests per second for 20 s each, then raise the rate in steps of 20 s. A figure is the highest step with no failed request and a p95 of under 400 ms end to end. Open loop: requests arrive at the target rate whether or not the previous ones have returned, so a server that falls behind is punished, not protected.
What limits each number
Section titled “What limits each number”- Lists are bound by bytes. A 30-record page is about 34 KB, 7 KB after gzip, and every one costs nginx a compression and the server a socket write. Smaller pages or fewer fields per record go further than any server tuning.
- Writes are one thread by design: SQLite has one writer, and Vaultbase groups the writes that arrive together into one commit.
- Reads scale with cores. The default starts cores minus two read workers (at least one); on this box that is two, with nginx and the main thread taking the other two. Three workers on this box measured worse than two, which is why the default leaves two cores free. The figures above predate v0.27.0, where on Linux the read workers also accept connections themselves instead of every request passing through the main thread.
- Single-record fetches are the cheapest request there is, about 2 µs of database time; the 3,000 figure is the test client’s ceiling.
Measuring your own
Section titled “Measuring your own”- Seed a collection large enough that cold reads matter (100,000 records is a fair start).
- Let the Monitoring page’s List queries panel tell you which indexes the queries you care about need, and create them from there.
- Warm up, then ramp. Read the first failed step, not the last clean one, and keep p95 in view: a server can keep up with the rate for a while after latency has gone bad.
- Measure from where your users are. The same server gave different latencies from two networks on the same day; the rates did not change.
What not to compare: numbers taken on a laptop (no proxy, no TLS, no network), numbers from a single endpoint presented as “requests per second”, or anything without an error count. A rate with errors is not a rate.