Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

perf: background write batcher for DuckDB telemetry ingest - #272

#272
Open
jstojiljkovic wants to merge 36 commits into
feature/duckdb-benchmark-blogtracewayapp/traceway:feature/duckdb-benchmark-blogfrom
feat/write-optimizationtracewayapp/traceway:feat/write-optimizationCopy head branch name to clipboard
Open

perf: background write batcher for DuckDB telemetry ingest#272
jstojiljkovic wants to merge 36 commits into
feature/duckdb-benchmark-blogtracewayapp/traceway:feature/duckdb-benchmark-blogfrom
feat/write-optimizationtracewayapp/traceway:feat/write-optimizationCopy head branch name to clipboard

Conversation

@jstojiljkovic

@jstojiljkovic jstojiljkovic commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

DuckDB ingest did all storage work inside each request: one connection, one Appender, one WAL commit per HTTP request, with the admission gate capping concurrency at 4. Requests held slots for 1-3s each. At the benchmark cliff, ingest p50 was 1.2-3.2 seconds.

What changed

  • Per-table background writers (telemetry/duckdb/writer.go): requests decode, convert, and enqueue onto a bounded per-table queue, then return 200 in milliseconds; a pool of writers per hot table (2xCPU, capped 8) owns persistent Appenders and flushes at 32k rows / 100ms, so one WAL commit is shared by hundreds of requests. A 200 now means "accepted into the queue" (OTel-Collector-style ack, loss window ≤ one flush on crash).
  • Reject-before-ack backpressure: a full queue waits up to 2s for space (VM-style), then answers 503 + Retry-After. Acked rows are never silently dropped. Queue depth is exposed in /api/health/deep and traceway.duckdb.write_queue.* metrics.
  • Native UUID columns (migration 0002): hot-table ids stored as 16-byte UUIDs instead of VARCHAR(36); converters pass duckdb.UUID zero-alloc.
  • Pooled decode scratch: sync.Pool for gzip readers and OTLP body buffers; memoized resource/scope attribute marshals in the logs converter.
  • Read-your-writes preserved: the notification event evaluator flushes writers before reading back just-ingested rows; sessions, profiling_stacks, and exception_stack_traces stay synchronous.

Results:

Same hardware (ccx13, 2 vCPU) - datasets committed on this branch:

Metric spans metrics logs
Sustained ingest (items/s) 95,737 -> 143,620 (+50%) 254,242 -> 322,994 (+27%) 75,225 -> 77,521 (+3%)
Ingest p50, best passing bulk step 1,167ms -> 589ms 3,207ms -> 78ms 2,958ms -> 161ms
Read-probe median @ 10M rows 902ms -> 637ms 382ms -> 314ms 85ms -> 55ms

Hardware scaling (same code, ccx23, 4 vCPU), artifacts on runs
30009675827 / 30028543147

Metric spans metrics logs
Sustained ingest (items/s) 285,723 (1.89x) 685,935 (2.12x) 155,870 (1.95x)

Writers, admission, and DuckDB threads all scale with CPU, so capacity scales near-linearly with cores - previously added cores mostly fed per-request overhead. On both tiers, the load generator saturates before the SUT on spans/metrics small-batch traffic (0% SUT errors at the failed loadgen steps).

Zero dropped rows and zero insert failures in every step of every run. Read-probe pass levels are unchanged; the spans/logs 100M read cliffs predate this branch

dusanstanojeviccs and others added 30 commits July 20, 2026 18:16
blog: SQLite vs DuckDB benchmark + ingest admission gate
@jstojiljkovic jstojiljkovic self-assigned this Jul 27, 2026
@jstojiljkovic
jstojiljkovic marked this pull request as ready for review July 27, 2026 12:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.