atproto pds in zig pds.zat.dev
pds atproto
30

Configure Feed

Select the types of activity you want to include in your feed.

README.md

benchmarks#

bench/ contains local benchmarks for ZDS storage paths. The goal is to track scaling curves and compare protocol work against reference implementations, not to produce a single universal score.

The benchmark style follows atproto-bench: state the exact work being measured, keep correctness work visible, compare equivalent operations, and treat run-to-run variance as real.

run#

just bench all
just bench write
just bench read
just bench repo
just bench blob
just bench metastore 10 1000
just bench get-cid 10 1000
just bench get-block 10 1000
just bench decode-record 10 1000
just bench render-record 10 1000
just bench get-record 10 1000
just bench list-records 10 1000
just bench official-pds
just bench write-profile 10 500
just bench run --scenario write --records 10000

just bench ... is a Just module declared from the repository root with mod bench.

current scenarios#

  • write: applyWrites creating app.bsky.feed.post records.
  • read: repeated listRecords queries against one seeded repo.
  • repo: full repo CAR materialization through writeRepoCar.
  • blob: blob write/read against disk blobstore plus SQLite metadata.
  • metastore: Tranquil-shaped apply/get/list benchmark with caller counts and latency percentiles.
  • get-cid: CID-only record lookup, matching Tranquil's get_record_cid metastore benchmark.
  • get-block: record index lookup plus repo_blocks byte fetch.
  • decode-record: DAG-CBOR decode of one seeded record block, without SQLite.
  • render-record: DAG-CBOR decode plus JSON rendering, without SQLite.
  • get-record: full com.atproto.repo.getRecord storage materialization.
  • list-records: full com.atproto.repo.listRecords storage materialization.
  • official-pds: copies the official-PDS read probe into a local bluesky-social/atproto checkout and runs it with Vitest.
  • write-profile: applyWrites stage timing under write concurrency.

Each run uses temporary SQLite and blobstore state.

methodology#

  • Build ZDS with -Doptimize=ReleaseFast.
  • Use temporary SQLite and blobstore state for every run.
  • Seed records before read benchmarks so lookup/list work is separated from write setup.
  • Report throughput plus p50/p95/p99/max/mean latency for concurrent paths.
  • Compare ratios from the same machine and same run window. Absolute numbers move with system load, thermal state, compiler version, and storage backend.
  • Do not compare paths that perform different correctness work. For example, CID-only metadata lookup is separate from full getRecord materialization.

traffic model#

For a small hosted PDS, the hot paths are not exotic. A hundred active accounts mainly means more session checks, appview-proxied feed/profile reads, record writes from clients and apps, blob uploads, sync subscribers, crawl requests, and occasional repo export/import. The storage benchmarks should therefore keep these domains separate:

  • record index: current CID lookups, existence checks, and index-only lists.
  • record materialization: index lookup, block fetch, DAG-CBOR decode, and JSON response rendering.
  • write path: validation, repo mutation, commit/block persistence, event publication, and crawl notification.
  • blob path: upload body ingest, disk write, metadata persistence, and readback.
  • sync path: event delivery and repo CAR materialization.

current local baseline#

On this machine, with --records 1000 unless noted:

scenario zds
write records 144 ops/s
list records 30k ops/s
write repo car 252 ops/s, 440 MB/s
blob put+get 368 ops/s, 46 MB/s

reference matrix#

Run Tranquil's metastore benchmark:

cd /tmp/tranquil-pds
SQLX_OFFLINE=true cargo bench -p tranquil-store --bench metastore --features tranquil-store/test-harness

Run the matching ZDS scenarios:

just bench metastore 10 1000
just bench metastore 100 200
just bench get-cid 10 1000
just bench get-cid 100 200

This matrix includes only operation/count pairs measured with the same unit of work. Tranquil's get_record_cid is a record-index operation. The official PDS probe measures its actor-store read paths with Vitest; its single-operation rows are included for matching internal read/write boundaries.

Summary: ZDS is fastest in the single-caller read probes. Tranquil is fastest in the direct 10/100 caller CID lookup probes. The official PDS is slower in these microbenchmarks because the probe goes through its actor-store/test-network stack rather than a bare SQLite reader. For writes, ZDS is fastest in the single-caller row, Tranquil is faster than ZDS at 10 callers, and ZDS is faster than Tranquil at 100 callers but with a worse p99/max tail.

operation callers zds tranquil official PDS
apply one record commit 1 642 ops/s, p95 2.9 ms 287 ops/s, p95 4.1 ms 448 ops/s, p99 4.0 ms
current record CID lookup 1 414k ops/s, p95 4 us 96.8k ops/s, p95 15 us 3.1k ops/s, p99 511 us

The direct caller-count concurrency matrix is ZDS and Tranquil only. Official PDS concurrency probes are kept separate until they use the same caller model.

operation callers ops zds tranquil
apply one record commit 10 10000 259 ops/s, p95 98.9 ms 780 ops/s, p95 14.1 ms
apply one record commit 100 20000 1223 ops/s, p95 67.5 ms 1007 ops/s, p95 109 ms
current record CID lookup 10 10000 222k ops/s, p95 225 us 455k ops/s, p95 43 us
current record CID lookup 100 20000 211k ops/s, p95 887 us 537k ops/s, p95 233 us

Official PDS batched CID lookup:

operation batch shape converted throughput batch p99
current record CID lookup 10 callers x 100 lookups 3.3k lookups/s 317 ms
current record CID lookup 100 callers x 20 lookups 3.2k lookups/s 630 ms

Tranquil's metastore list_records benchmark is not in this table because the ZDS row reports the official-PDS-shaped listRecords: record index lookup, repo_blocks fetch, DAG-CBOR decode, and JSON materialization. Add an index-only ZDS list probe before comparing that row.

official PDS probe#

The official-PDS probe lives at bench/official-pds-records.bench.ts and is run with:

ATPROTO_REPO=/tmp/bluesky-atproto just bench official-pds

It seeds 1000 app.bsky.feed.post records through the official test network, then measures actor-store reads directly:

operation official PDS
apply one record commit 448 ops/s, p99 4.0 ms
current record CID lookup 3.1k ops/s, p99 511 us
full getRecord materialization 3.0k ops/s, p99 525 us
full listRecords, limit 50 635 ops/s, p99 2.4 ms

The matching ZDS single-caller rows are:

operation zds
apply one record commit 642 ops/s, p95 2.9 ms
current record CID lookup 414k ops/s, p95 4 us
full getRecord materialization 48.6k ops/s, p95 28 us
full listRecords, limit 50 21.6k ops/s, p95 60 us

local probes#

These rows are useful for ZDS tuning but are not direct Tranquil comparisons.

operation callers ops zds
get record block 10 10000 128k ops/s, p95 11 us
get record block 100 20000 121k ops/s, p95 15 us
decode DAG-CBOR 10 10000 40.6M ops/s, p95 below timer resolution
decode DAG-CBOR 100 20000 12.6M ops/s, p95 below timer resolution
render record JSON 10 10000 24.8M ops/s, p95 below timer resolution
render record JSON 100 20000 12.4M ops/s, p95 1 us
full getRecord 10 10000 38.4k ops/s, p95 173 us
full getRecord 100 20000 38.1k ops/s, p95 11.7 ms
full listRecords, limit 50 10 10000 21.1k ops/s, p95 57 us
full listRecords, limit 50 100 20000 19.5k ops/s, p95 78 us

The read-path split says SQLite serialization and row materialization dominate single-record reads. Decode/render is effectively invisible for tiny benchmark records; it needs a larger-record probe before we can extrapolate image-heavy or rich-record traffic.

write profile#

write-profile tracks where concurrent writes spend time:

callers ops throughput lock wait load repo build commit sqlite/event
10 5000 708 ops/s 25% 26% 3% 46%
100 2000 3683 ops/s 64% 10% 1% 25%

ZDS separates repo ordering from database serialization: writes take a per-DID lane before loading and mutating the repo, then use the DB mutex for the shared SQLite connection. Tranquil remains the reference shape for bounded write queues, explicit backpressure, less full-repo state loading, and a narrower SQLite persistence phase.

comparison work#

Tranquil benchmark source:

Official PDS source for matching probes:

Open apples-to-apples comparison gaps:

  • single-repo write curve as record count grows
  • multi-repo concurrent writes
  • record-index list latency without record body materialization, across ZDS and Tranquil
  • official-PDS direct concurrency harness for the same read/write boundaries
  • full repo export cost
  • blob/block store throughput

ZDS and Tranquil both report latency percentiles for the metastore-shaped write and CID lookup rows. The remaining comparison gap is broader coverage of blockstore, eventlog, full repo export behavior, an official-PDS benchmark harness, and an explicit ZDS index-only list probe that matches Tranquil's list_records.