···77Current shape:
8899- ZDS serves raw CAR bytes and does not appear to add application-layer gzip, so it avoids the Node compression issue described in the operator thread.
1010-- Full repo export is built from SQLite `repo_blocks`, ordered by CID.
1010+- As of the 2026-06-29 getRepo pass, full repo export starts at the latest commit block and uses `zat.mst.collectReachableBlocks` to include only current MST and record blocks reachable from the commit's `data` CID.
1111- Incremental export with `since` filters by `repo_rev`.
1212- Repo writes already use lazy MST loading backed by `repo_blocks`, so ZDS is closer to a Hubble-style block/CAR-serving shape than an eager full-repo rebuild path.
1313-- `subscribeRepos` has a connection cap, but `getRepo` does not appear to have route-specific backpressure.
1313+- `subscribeRepos` has a connection cap. Full `getRepo` now has its own lightweight route-local concurrency cap so backup/backfill exports cannot consume unbounded handler slots. Operators can tune it with `ZDS_MAX_CONCURRENT_REPO_EXPORTS`; the default is `4`, matching Tranquil's current full-export default.
14141515Open questions:
16161717-- Should full `getRepo` walk the current commit/MST and include only reachable blocks, instead of exporting every retained block for the DID?
1818-- Do retained old MST or record blocks make the current full export disclose deleted or stale record data?
1919-- Should `getRepo` have route-specific concurrency or rate limits separate from normal public reads?
2020-- Should the benchmark suite compare the current range-scan export against a reachability-based export for small, medium, and large repos?
1717+- Should `HEAD com.atproto.sync.getRepo` stay semantically cheap, or should it compute/export-sized metadata? Today it is treated as a probe and still shares the same handler path.
1818+- Should `getRepo` backpressure eventually become per-account/IP rate limiting in addition to process-local concurrency?
1919+- Should the benchmark suite keep a historical range-scan comparison around, now that production full export is reachability-based?
2020+2121+Done in this pass:
2222+2323+1. Adopted `zat` 0.3.9 for `collectReachableBlocks`.
2424+2. Added a regression test for update/delete followed by full `getRepo`; stale record CIDs are excluded from full export.
2525+3. Changed full export from whole-DID `repo_blocks` scan to current-root reachability.
2626+4. Added lightweight route backpressure for full `getRepo`, configurable with `ZDS_MAX_CONCURRENT_REPO_EXPORTS`.
2727+5. Ran `just bench repo-size`; reachable full export measured about 1.4 ms for 100 records, 16.8 ms for 1,000 records, and 95.7 ms for 5,000 records on the local synthetic benchmark.
21282229Suggested next pass:
23302424-1. Add a regression test for delete/update followed by full `getRepo`, checking whether stale record blocks are exported.
2525-2. If stale blocks are visible, change full export to current-root reachability rather than whole-DID block scan.
2626-3. Add lightweight route backpressure for full `getRepo`.
2727-4. Re-run `bench repo-size` and keep raw range-scan numbers separate from reachable-export numbers.
3131+1. Decide whether `GETREPO_NOTES.md` should stay as a root handoff note or move into `docs/`.
3232+2. Add a larger deliberate `just bench repo-large 100000` run when we want stress numbers comparable to large public repos.
···107107fixtures before they become stable benchmarks. Do not make routine CI or
108108pre-commit checks depend on live PDS availability.
109109110110+June 29, 2026 local reachable-export check after switching full
111111+`com.atproto.sync.getRepo` from whole-DID `repo_blocks` scans to current-root
112112+reachability via `zat.mst.collectReachableBlocks`:
113113+114114+| repo tier | records | list | full CAR export |
115115+|---|---:|---:|---:|
116116+| small | 100 | 13.4k ops/s, 15.0 ms | 3.6k ops/s, 72.1 MB/s, 1.4 ms |
117117+| medium | 1,000 | 12.4k ops/s, 16.1 ms | 297 ops/s, 59.6 MB/s, 16.8 ms |
118118+| large | 5,000 | 11.9k ops/s, 16.8 ms | 52 ops/s, 53.1 MB/s, 95.7 ms |
119119+120120+These are synthetic local trend numbers, not cross-PDS comparisons. The old
121121+whole-DID range-scan export path is intentionally no longer the production full
122122+export behavior because it could include retained stale blocks.
123123+110124## methodology
111125112126- Build ZDS with `-Doptimize=ReleaseFast`.
···105105- `ZDS_BLOB_UPLOAD_LIMIT`: upload body limit. Default: `100000000`.
106106- `ZDS_BLOBSTORE_PATH`: disk blobstore root.
107107- `ZDS_CRAWLERS`: comma-separated relay crawl targets.
108108+- `ZDS_MAX_CONCURRENT_REPO_EXPORTS`: maximum concurrent full
109109+ `com.atproto.sync.getRepo` exports. Default: `4`. Incremental
110110+ `getRepo?since=...` exports are not intended to consume this full-export
111111+ backpressure budget.
108112- `ZDS_ADMIN_TOKEN`: bearer token for privileged local PDS administration,
109113 including invite-code minting.
110114- `ZDS_INVITE_REQUIRED`: set to `true` to require invite codes for account
···3939 if (options.blobstore_path) |value| zds.core.config.setBlobstorePath(value);
4040 if (options.handle_domains) |value| zds.core.config.setHandleDomains(value);
4141 if (options.crawlers) |value| zds.core.config.setCrawlers(value);
4242+ if (options.max_concurrent_repo_exports) |value| zds.core.config.setMaxConcurrentRepoExports(value);
4243 if (options.proxy_service_did) |value| zds.core.config.setProxyServiceDid(value);
4344 if (options.proxy_service_id) |value| zds.core.config.setProxyServiceId(value);
4445 if (options.proxy_service_url) |value| zds.core.config.setProxyServiceUrl(value);