Benchmarks
Sairo ships with a benchmark suite you can run against your own deployment. The numbers below come from two complementary contexts, kept deliberately separate so they are never conflated:
- Server-compute microbenchmarks — single Uvicorn process in Docker against local MinIO. These isolate Sairo’s own latency (index lookups, FTS5, SQL aggregation) with negligible network overhead.
- Production validation — a live, read-only run against a real multi-hundred-terabyte deployment, to confirm the architecture holds at scale.
Production Validation (live deployment)
Section titled “Production Validation (live deployment)”Measured read-only against a live production instance on Leaseweb S3-compatible object storage:
| Parameter | Value |
|---|---|
| Total indexed | 15.5M objects across 14 buckets, ~269 TB |
| Largest single bucket | ~9.86M objects |
| Access path | Public internet, behind a CDN/TLS edge |
Because this run crosses the public internet, every request carries a fixed transport round-trip (RTT) floor. We measured that floor against a zero-compute endpoint and subtract it to isolate Sairo’s server-side cost:
| Operation (on the ~9.86M-object bucket) | End-to-end p50 | Server compute (end-to-end − RTT floor) |
|---|---|---|
/healthz (zero-compute reference) | 284ms | — (this is the RTT floor) |
| Folder listing | 281ms | ≈ 0ms — at the network floor |
| Search (200 results) | 305ms | ~20–35ms |
| Storage breakdown (root) | 304ms | ~20–35ms |
The headline result: folder listing on a ~9.86M-object bucket lands at the network floor — Sairo adds no measurable server-side latency, because the listing is a single indexed lookup against a pre-computed prefix hierarchy regardless of folder size. Search and storage breakdown stay in the tens of milliseconds server-side at ~10M objects.
Test Environment (microbenchmarks)
Section titled “Test Environment (microbenchmarks)”| Parameter | Value |
|---|---|
| Runtime | Docker container, single Uvicorn process |
| Host | macOS, Apple Silicon (Docker Desktop) |
| Storage | Local MinIO, plus a local Sairo pointed at production S3 indexes |
| Methodology | 15–30 iterations per measurement, percentile reporting |
Search Latency (server-compute)
Section titled “Search Latency (server-compute)”FTS5 trigram search, measured locally to isolate server cost:
| Dataset | Query | Results | p50 | p95 |
|---|---|---|---|---|
| 134K-object index | typical (limit=200) | 200 | 2.2–3.1ms | 3–22ms |
| 134K-object index | broad match, limit=500 | 500 | 40ms | 43ms |
| ~10M-object bucket (live, minus RTT) | 200 results | 200 | ~20–35ms | — |
Typical queries return in single-digit milliseconds on hundreds of thousands of objects, and in tens of milliseconds on ~10M-object buckets. Larger result sets (500+ rows) cost more because of row serialization, not search.
Object Listing & Pagination
Section titled “Object Listing & Pagination”Listing is served from the SQLite index, never a live S3 LIST. Folder navigation uses a pre-computed prefix_children hierarchy, so it is a constant-time index lookup at any folder size.
| Scenario | Before | After | Notes |
|---|---|---|---|
| Folder listing, 557K-object bucket | 114ms | 0.048ms | pre-computed prefix hierarchy |
| Folder listing, >1M-object bucket | OOM (disabled) | constant-time | previously turned off above 1M objects; now lands at the network floor on a live 9.86M-object bucket |
| Leaf listing (large folder), paginated | 8.7ms, ~1 MB payload | 4.8ms, ~238 KB payload | pagination (v3.3.0) caps payload + latency |
| Flat folder with 1,000,000 objects in one prefix | — | ~1.3s | worst case: a single non-hierarchical folder of 1M keys |
The flat-1M-folder case is the deliberate stress test — a single prefix holding a million keys with no sub-structure. Even then it returns in ~1.3s; any normally-partitioned layout lists in low single-digit milliseconds.
Indexing Throughput
Section titled “Indexing Throughput”| Bucket | Objects | Throughput |
|---|---|---|
| bench-small | 1,000 | 926 obj/s |
| bench-mixed | 2,416 | 1,348 obj/s |
| production buckets | 15.5M (live) | crawled + kept fresh via adaptive delta crawler |
Indexing rate: 1,000–1,350 objects/second on local MinIO. At production scale, large buckets are kept current by an adaptive delta crawler that re-lists only the newest partitions instead of re-walking the whole bucket each cycle, with periodic full reconciles.
Uploads (direct browser → S3)
Section titled “Uploads (direct browser → S3)”Since v3.4.0, uploads go directly from the browser to the S3 endpoint — file bytes never pass through the Sairo server. Small files use a single presigned PUT; large files use presigned multipart, splitting the file into parts uploaded in parallel, each part signed just-in-time.
| Property | Behavior |
|---|---|
| Server memory during upload | Flat, independent of file size — no buffering, so no OOM or pod restarts at scale |
| Maximum object size | Up to S3’s 5 TB per-object limit (no single-PUT ceiling) |
| Large-file transfer | Parallel multipart parts; an in-progress upload can be stopped and is cleanly aborted on S3 |
| Throughput | Bounded by the client’s bandwidth to S3, not by Sairo |
API Response Times (server-compute)
Section titled “API Response Times (server-compute)”| Endpoint | p50 | p95 |
|---|---|---|
/healthz | 2.1ms | 3.6ms |
/api/buckets | 4.3ms | 5.8ms |
| Object listing | 2.4ms | 4.6ms |
| Storage breakdown (root) | 2.4–3.9ms | 5–6ms |
| Presigned URL generation | 3.1ms | 5.6ms |
Sub-5ms p50 for standard endpoints on local hardware.
Concurrent Load
Section titled “Concurrent Load”Concurrent search queries, single Uvicorn process:
| Concurrent Users | Requests/sec |
|---|---|
| 5 | 236 |
| 10 | 333 |
| 25 | 528 |
Throughput scales with worker count; these are single-process numbers.
SQLite PRAGMA Tuning
Section titled “SQLite PRAGMA Tuning”The index database is opened with: cache_size=-64000 (64 MB), mmap_size=268435456 (256 MB memory-mapped I/O), temp_store=MEMORY. Impact at scale:
| Query | Before | After | Dataset |
|---|---|---|---|
COUNT(*) | 11.3ms | 7.3ms | 2M objects |
COUNT(*) | 2.0ms | 1.5ms | 557K objects |
| Folder stats rebuild | 63ms | 56ms | 139K objects |
Crawl Architecture
Section titled “Crawl Architecture”| Setting | Value |
|---|---|
| Crawl workers | 12 |
| Prefix workers | 16 |
| Batch insert size | 10,000 |
| FTS rebuild | Background thread (never blocks listing/search) |
| Large buckets | Adaptive delta crawl + periodic full reconcile |
| Sub-prefix splitting | Automatic for buckets with few top-level prefixes |
Running the Benchmarks
Section titled “Running the Benchmarks”The benchmark suite lives in the benchmark/ directory of the repository.
1. Seed test data
Section titled “1. Seed test data”# Requires MinIO CLI (mc) configured with alias "local"cd benchmark./seed-data.sh # Seeds bench-small (1K) + bench-mixed (2.4K)./seed-data.sh medium # Seeds bench-medium (10K objects)./seed-data.sh large # Seeds bench-large (50K objects)| Bucket | Objects | Pattern |
|---|---|---|
bench-small | 1,000 | 5 dirs × 10 months × 20 files |
bench-mixed | ~2,400 | Parquet data lake, logs, configs, CSV reports |
bench-medium | 10,000 | 10 dirs × 10 months × 10 sub-dirs × 10 files |
bench-large | 50,000 | 10 dirs × 50 partitions × 100 records |
2. Run benchmarks
Section titled “2. Run benchmarks”./run-benchmarks.sh # all categories./run-benchmarks.sh search # search latency only./run-benchmarks.sh crawl listing # crawl + listingPrerequisites: Sairo on localhost:8000 (or set SAIRO_URL), MinIO on localhost:9000, test buckets seeded, and admin credentials.
3. Results
Section titled “3. Results”Results are saved to benchmark/results/ as machine-readable JSON and a human-readable LATEST-RESULTS.md.
Landing Page Claims
Section titled “Landing Page Claims”| Landing Page Claim | Benchmark Evidence |
|---|---|
| ”Proven at 269 TB / 15.5M objects” | Live deployment, 14 buckets, largest ~9.86M objects |
| ”Constant-time folder listing at any scale” | Listing lands at the network floor on a ~9.86M-object bucket |
| ”Single-digit millisecond search” | p50 = 2.2–3.1ms on 134K objects (tens of ms at ~10M) |
| “1,300+ obj/sec indexing” | 1,348 obj/s measured on bench-mixed |
| ”Sub-5ms API responses” | healthz p50 = 2.1ms, most endpoints < 5ms |
| ”500+ requests/second” | 528 req/s at 25 concurrent users |
| ”Uploads of any size, zero server memory” | Direct browser→S3 multipart up to 5 TB; flat server memory |