Benchmarks
Results of scripts/benchmark/run.py against scripts/benchmark/generate.py data, measured on the dev server (DigitalOcean, 4 vCPU, 8 GB, ams3) on 2026-09-05. The dataset was removed from the dev server on 2026-09-12, so this page is the record of that run; to refresh it, regenerate the data locally or on a throwaway server (uv run scripts/benchmark/generate.py --scale 0.2, --reset removes it again) and rerun run.py. Budgets come from architecture 13.7 and 13.8; they are development budgets, not service levels.
Last run: 2026-09-05 09:26 UTC.
Dataset
| The dataset of the run (2026-09-05; none of these rows exist any more) | Whole database | Benchmark project |
|---|---|---|
| Project | all | Benchmark Kruger |
| Devices | 2,001 | |
| Entities | 1,001 | 125 |
| Positions | 44,231,968 | 5,660,840 |
| Measurements | 176,927,824 | 22,643,360 |
Results
| Operation | Samples | p50 | p95 | Budget | Verdict | Notes |
|---|---|---|---|---|---|---|
| live map load | 5 | 39 ms | 101 ms | 3.0 s | within budget | |
| viewport tile | 3 | 24 ms | 29 ms | 2.0 s | within budget | |
| viewport tile | 3 | 21 ms | 25 ms | 2.0 s | within budget | |
| viewport tile | 3 | 20 ms | 20 ms | 2.0 s | within budget | |
| track 1 day | 5 | 33 ms | 125 ms | 2.5 s | within budget | 74 rows |
| track 30 days | 5 | 150 ms | 185 ms | 2.5 s | within budget | 4,048 rows |
| track 1 year | 5 | 566 ms | 862 ms | 3.0 s | within budget | 49,938 rows |
| explorer series, 1 metric, 30 days | 5 | 223 ms | 459 ms | 3.0 s | within budget | 2,221 rows |
| explorer series, 1 metric, 1 year | 5 | 904 ms | 1.7 s | 3.0 s | within budget | 1,459 rows |
| explorer series, 4 metrics x 5 entities, 7 days | 5 | 1.0 s | 1.1 s | 3.0 s | within budget | 11,616 rows |
| explorer metrics with data, 30 days | 3 | 1.5 s | 2.4 s | 3.0 s | within budget | |
| explorer drill-down page | 5 | 124 ms | 126 ms | 1.0 s | within budget | |
| direct export, positions, csv | 2 | 10.0 s | 10.3 s | 10.0 s | over budget | 39,453 rows |
| export job, measurements, csv, 1 year | 1 | 4359.1 s | 4359.1 s | 22,615,236 rows, 4868.2 MB, export container 107 to 129 MiB | ||
| ingest webhook request | 2000 | 713 ms | 1.4 s | 2,000 rows | ||
| ingest burst accepted | 1 | 49.7 s | 49.7 s | 2,000 rows, 40 events/s | ||
| ingest burst decoded (end to end) | 1 | 129.2 s | 129.2 s | 2,000 rows, 15 events/s | ||
| commit to canonical row (p95 from timestamps) | 1 | 76.6 s | 76.6 s | 2.0 s | over budget |
Notes
- Live map returned 125 features (mode geojson).
Reading the run
Measured on a 4 vCPU, 8 GB DigitalOcean droplet against a dataset at 0.2 of the reference envelope (decision D91): 2,000 devices, 44 million positions, 177 million measurements, compressed to 2.6 GB and 7.3 GB on disk. Every interactive read path is within its budget at twice the previous dataset on a machine slower than the development laptop. Three things stand out:
- Direct export at the edge. 39,453 positions to CSV took 10.3 s at p95 against a 10 s budget, five times the laptop's time for the same rows: the API serialises the rows on one vCPU. Above about 40,000 rows a job is the right path anyway (the 100,000 row cut-off of architecture 13.8); lowering the cut-off on small servers is the candidate change.
- Export jobs stream within a bounded memory. 22.6 million measurements, 4.9 GB of CSV, with the export worker between 107 and 129 MiB: the streaming writer holds (13.8). The throughput, 5,200 rows per second here against 30,000 on the laptop, sets the expectation that a year of one project's measurements is an hour's job on a small server.
- The decoder path is the bottleneck under a burst. 2,000 uplinks were accepted at 40 per second and decoded end to end at 15 per second, with a p95 of 77 s from webhook to canonical row against the 2 s budget (14 s on the laptop). At this scale the normal load is about 7 uplinks per second, so the pipeline keeps up in steady state and drains a burst in minutes; at the full envelope (about 33 per second) one decoder on this server would fall behind. The decoder consumes a Redis Streams consumer group, so the remedy is more decoder replicas or a larger server; profiling the per-event cost is the follow-up before 2.0 claims the full envelope.
The full-envelope figures remain extrapolated (D91): read paths scale with the bounded query shapes and were flat between 0.1 and 0.2; ingest scales with decoder replicas.
Earlier run on the development machine
2026-09-03 18:42 UTC, 0.1 of the envelope (22.4 million positions, 89.7 million measurements) on a laptop:
| Operation | Samples | p50 | p95 | Budget | Verdict | Notes |
|---|---|---|---|---|---|---|
| live map load | 5 | 72 ms | 246 ms | 3.0 s | within budget | |
| viewport tile | 3 | 18 ms | 52 ms | 2.0 s | within budget | |
| viewport tile | 3 | 14 ms | 14 ms | 2.0 s | within budget | |
| viewport tile | 3 | 5 ms | 5 ms | 2.0 s | within budget | |
| track 1 day | 5 | 11 ms | 61 ms | 2.5 s | within budget | 132 rows |
| track 30 days | 5 | 34 ms | 2.3 s | 2.5 s | within budget | 4,105 rows |
| track 1 year | 5 | 214 ms | 1.1 s | 3.0 s | within budget | 49,996 rows |
| explorer series, 1 metric, 30 days | 5 | 34 ms | 97 ms | 3.0 s | within budget | 2,235 rows |
| explorer series, 1 metric, 1 year | 5 | 51 ms | 946 ms | 3.0 s | within budget | 1,460 rows |
| explorer series, 4 metrics x 5 entities, 7 days | 5 | 216 ms | 674 ms | 3.0 s | within budget | 12,160 rows |
| explorer metrics with data, 30 days | 3 | 51 ms | 290 ms | 3.0 s | within budget | |
| explorer drill-down page | 5 | 14 ms | 87 ms | 1.0 s | within budget | |
| direct export, positions, csv | 2 | 2.0 s | 2.0 s | 10.0 s | within budget | 46,517 rows |
| export job, measurements, csv, 1 year | 1 | 381.8 s | 381.8 s | 11,375,836 rows, 2446.0 MB, worker resident memory 85 to 115 MiB (measured in a second run of the export section; the first run's docker stats figure counted the page cache of the 2.4 GB file) | ||
| ingest webhook request | 2000 | 144 ms | 262 ms | 2,000 rows | ||
| ingest burst accepted | 1 | 9.8 s | 9.8 s | 2,000 rows, 205 events/s | ||
| ingest burst decoded (end to end) | 1 | 24.8 s | 24.8 s | 2,000 rows, 81 events/s | ||
| commit to canonical row (p95 from timestamps) | 1 | 14.1 s | 14.1 s | 2.0 s | over budget |