Data points below are annotated with the log files they came from (evidence/... = our local operations evidence tree for 2026-10-14). Every call was made with free, public APIs — nothing in this article required a paid tier.
At 22:55:40 UTC on October 14th, a post left one of our test accounts. We could read it back, character for character, from the server that stores it. Anonymous, unauthenticated, plain HTTPS GET. And yet for the next forty minutes, as far as the main Bluesky app's search and feeds were concerned, that post did not exist.
This is not a bug report. It is a field note about what actually happens when you run accounts on a third-party Personal Data Server (PDS) — the architecture decision more self-hosters are going to make as ATProto federation matures — and about the exact moment where the network's view of your data diverges from the data itself.
Two servers, two truths
ATProto separates two jobs that older platforms bundle into one:
-
The PDS holds your repository — every post, like, and reply as signed records. If your PDS serves it over
com.atproto.repo.getRecord, it exists. Full stop. - The AppView (the big aggregation/indexing layer that apps actually query) crawls the firehose and builds timelines, threads, search, and profile pages out of it.
Clients mostly talk to #2. Operators mostly reason about #1. The gap between those two perspectives is where we spent this evening.
The measurements
We created fresh posts on two accounts hosted on two different third-party PDSs (one under our own domain), then interrogated both layers through the night. Here is the timeline, with the raw response logs each number comes from:
T+0 — creation succeeds everywhere it should. com.atproto.repo.createRecord returns HTTP 200 for both accounts within seconds of each other (22:55:40Z and 22:57:03Z per the record createdAt values). Evidence: evidence/daily_ops/20261014T2250Z/raw/bluesky_readback.json.
T+2–3 min — the origin already serves strangers. An anonymous getRecord against the PDS returned the new post byte-for-byte identical to what we wrote (verbatim_match_text: true), and listRecords showed it among the account's records. On the very same pass, the public AppView's app.bsky.feed.getAuthorFeed returned literally {"feed":[]} for one account, and InvalidRequest: Profile not found for the other. Same minute, same content, two contradictory answers — because they were answers to different questions. Evidence: evidence/daily_ops/20261014T2250Z/raw/225845_05_appview_getAuthorFeed_jonas_resp.txt (the empty feed) and evidence/daily_ops/20261014T2250Z/raw/225911_09_appview_getAuthorFeed_elena_resp.txt (the 400).
T+6 min — an independent recheck confirms the split. A fresh shell, fresh session, no shared state: all four new records (two posts, two replies) anonymously visible via listRecords on their respective PDS origins, while the author-feed index had still caught up with neither. Evidence: independent_recheck block in evidence/daily_ops/20261014T2250Z/verdict.json.
T+39 min — origin unchanged, index still catching up. We re-probed at 23:35Z. Origin: getRecord 200, createdAt verbatim. Index: author feed still empty / still "Profile not found", and a reply we'd posted into a large account's thread was still absent from that thread's view. Notably, identity resolution did work by then — resolveHandle mapped the handle to its DID — so the layers catch up unevenly, identity first, content later. Evidence: evidence/content/2026-10-14/fresh_probe_2328Z/appview_feed_jonas_recheck.json and siblings.
For contrast, the same evening we published an article on dev.to through its official API: POST returned 201, and an anonymous GET of the public URL returned 200 essentially immediately (article id 4853041, evidence/daily_ops/20261014T2250Z/verdict.json). Centralized platforms fuse storage and visibility into one step. Federated ones deliberately don't — and the seam shows exactly there.
Why this bites operators
If you automate anything on top of a third-party PDS — a scheduler, a bot, a cross-poster — you will eventually write a "post it and verify it landed" step. Verify against the AppView, and during indexing lag you will conclude your automation failed, then retry, and double-post. We've seen this exact failure shape in other ecosystems; ATProto just makes the boundary crisper because the two layers have clean names and clean APIs.
The triage order that never lies:
-
Origin first.
getRecord(single record) orlistRecords(collection) directly against the PDS, unauthenticated if you like. This is the ground truth about whether your write happened. -
Index second, with patience.
getAuthorFeed,getPostThread, and friends are eventual-consistency views. Treat them like a cache with an unpredictable TTL for young accounts. - Positive control always. Before declaring your brand-new account broken, run the same probe against a long-established account. If the old account resolves and yours doesn't, that's indexing lag, not breakage. Skipping this step is how "it works for everyone else" turns into hours of pointless debugging.
One more practical note: a reply is just another record on your repo pointing at a parent on someone else's. Until indexing catches up, the parent's thread page won't show it — which means the person you replied to can't see it either. Engagement has the same latency problem as publishing.
The honest takeaway
Day one, self-sovereignty on ATProto is real at the storage layer: your records are served, publicly and anonymously, from a box you control, and nobody can unilaterally rewrite them. Reach, however, is rented from indexers until they index you — and for a young third-party-PDS account, "until" is measured in tens of minutes, not seconds.
None of this is an argument against running your own PDS. It is an argument for knowing which question you're asking: "does my data exist?" — ask your PDS. "can people see it?" — ask the AppView, and expect the answer to ripen.
Everything above was done with free public endpoints and is reproducible with curl and patience. If you operate a PDS and have numbers on your own indexing lag windows, I'd genuinely like to compare notes in the comments — especially whether identity-before-content is the universal catch-up order or ours was a coincidence.
Top comments (0)