Consuming the firehose
This page picks up from the Relay overview and walks through
actually consuming the firehose: opening the connection, decoding events, and
processing them in order. We use Bluesky's relay
(relay1.us-east.bsky.network) in the examples, but the same code works against
any relay.
The firehose is a binary stream: efficient, but more work to decode. If you'd rather receive filtered JSON and skip the CBOR/CAR decoding entirely, use Jetstream.
Open the connection
The firehose is served from the com.atproto.sync.subscribeRepos endpoint over
a WebSocket. No authentication is required.
- Go
- Shell
uri := "wss://relay1.us-east.bsky.network/xrpc/com.atproto.sync.subscribeRepos"
con, _, err := websocket.DefaultDialer.Dial(uri, http.Header{})
websocat wss://relay1.us-east.bsky.network/xrpc/com.atproto.sync.subscribeRepos
Each message that arrives is binary-encoded CBOR. Commit events carry a CAR slice of the repository holding the changed records. Most SDKs provide a wrapper that handles the framing and decoding for you. Our Go SDK is currently the most feature-complete for working with the firehose directly.
Handle each event
Once the bytes are decoded, you work with a stream of repository operations: things like "create post", "create like", or "delete follow". The example below sets up a handler that prints each operation as it arrives:
- Go
rsc := &events.RepoStreamCallbacks{
RepoCommit: func(evt *atproto.SyncSubscribeRepos_Commit) error {
fmt.Println("Event from ", evt.Repo)
for _, op := range evt.Ops {
fmt.Printf(" - %s record %s\n", op.Action, op.Path)
}
return nil
},
}
sched := sequential.NewScheduler("myfirehose", rsc.EventHandler)
events.HandleRepoStream(context.Background(), con, sched)
Here we set up a sequential scheduler, which handles events one at a time, in order. Other schedulers run handlers in parallel, or do limited concurrency keyed on the event's repo. This is useful once a single-threaded handler can't keep up with the network's volume, while still preserving per-repo ordering.
HandleRepoStream does the actual decoding of the data coming over the
WebSocket and dispatches each event to the handler you wrote.
Keep your place in the stream
The firehose is sequenced: every event carries a monotonically increasing
cursor. If your consumer disconnects, you can reconnect and pass the last cursor
you processed back as a cursor query parameter to resume roughly where you
left off, rather than missing everything that happened while you were away.
wss://relay1.us-east.bsky.network/xrpc/com.atproto.sync.subscribeRepos?cursor=<seq>
Relays only buffer a limited backlog, so if you're offline long enough to fall outside that window, resuming by cursor alone isn't enough; you need to catch up from history. How you do that comes down to whether your position in the stream is stateless or stateful.
A stateless catch-up derives the gap on demand and forgets it afterward: Jetstream's network replay streams history and cuts back over to live in one pass, but tracking what you've processed is on you.
To persist your position across restarts, you need a stateful cursor. Tap is the recommended tool for long-lived cursor management. It sits in front of the firehose and keeps a durable, per-repo cursor: you subscribe to the repos you care about, and Tap handles the connection, verification, backfill, and buffering — delivering historical events first, then cutting over to the live tail. As you acknowledge events, Tap advances your cursor, so a restart resumes exactly where you left off.
go install github.com/bluesky-social/indigo/cmd/tap@latest
# Start Tap and open its event channel
tap run
websocat ws://localhost:2480/channel
# Subscribe to a repo — Tap backfills it, then streams live
curl -X POST http://localhost:2480/repos/add \
-H "Content-Type: application/json" \
-d '{"dids": ["did:plc:ewvi7nxzyoun6zhxrhs64oiz"]}'
Tap builds on the same repository backfill primitives described in atproto's Backfilling guide.
Either approach becomes more robust against a Sync 1.1
endpoint: the prevData field on each commit lets
you detect gaps and verify you haven't missed an update for a given repo.
See also
- Relay: what the relay is and which endpoints Bluesky runs.
- Jetstream: the filtered JSON alternative.
- Tap and the Backfilling guide: stateful cursor management and catching up from history.
- Event Stream spec and Sync spec: the protocol-level details.