メインコンテンツまでスキップ

Jetstream

Jetstream is the easiest way to get data off the AT Protocol network at scale. Filter the records you want (likes, posts, a single account), and Jetstream streams them as plain JSON over one WebSocket, the moment they happen.

Jetstream delivers data live, or replayed from history, or both at once, through the same interface. The same code works whether you're building a realtime bot or replaying a month of history into a larger application.

There are three ways to consume it:

  • Live tail — a single WebSocket that streams events the moment they happen.
  • Replay — the WebSocket plus a few HTTP calls to pull history, then cut over to live.
  • Snapshotting — a point-in-time copy of the archive over HTTP, with no live tail.

Jetstream is open source, implemented in Go, and cheap to self-host. Read on to interact with Bluesky's public instances.

Quickstart: live tail

Open a WebSocket to the subscribeEvents endpoint and start reading events. Filter as needed:

Use the Jetstream SDK:

// npm install @bsky/jetstream
import { Jetstream } from '@bsky/jetstream'

const jetstream = new Jetstream('https://jetstream.us-east.bsky.network')

for await (const evt of jetstream.live({ collections: ['app.bsky.feed.post'] })) {
console.log(evt)
}

Each message is a self-contained JSON object: an envelope with the event under payload, tagged by a $type. A commit event — someone creating, updating, or deleting a record — looks like this:

{
"$type": "message",
"payload": {
"$type": "network.bsky.jetstream.subscribeEvents#commit",
"did": "did:plc:7e6kocyzb77xkncplrkoojej",
"seq": 24664288881,
"time": "2026-08-13T06:47:43.959305Z",
"operation": "create",
"collection": "app.bsky.feed.like",
"rkey": "3msx2efqdxs27",
"rev": "3msx2efqjtc27",
"cid": "bafyreigwnxqttkhzha2ig4io6wwht3qiugtor4ruglceyfdbnyq53a55fe",
"record": {
"$type": "app.bsky.feed.like",
"createdAt": "2026-08-13T06:47:44.859Z",
"subject": {
"cid": "bafyreibfn4d6xedoe6jixkcl266v2fhiwga2x3zzsnq6yawnago2kb5ouu",
"uri": "at://did:plc:gdcz3s2rofr4bbom5bx3kqrr/app.bsky.feed.post/3mjfhkjsshs2q"
}
}
}
}

The record arrives already decoded in the record field, so there's no second parsing step. A delete carries no record or cid — just the collection and rkey that identify what went away. Alongside commit events, the stream also carries identity, account, and sync events, each with its own $type and a payload field of the same name.

Public endpoints

Bluesky runs public v2 instances in two regions. Use these for new projects — they serve the live tail shown above, plus Replay and Snapshotting.

EndpointRegion
wss://jetstream.us-east.bsky.networkUS East
wss://jetstream.us-west.bsky.networkUS West

The live tail path on these hosts is /xrpc/network.bsky.jetstream.subscribeEvents, and its cursor is the seq on every event (see Resuming).

No authentication is required for the live tail; these instances serve the full network.

Legacy v1 instances

The older jetstream1 and jetstream2 hosts in each region (e.g. wss://jetstream1.us-east.bsky.network) still serve the frozen v1 wire at /subscribe, and the v2 hosts above serve it too so existing consumers keep working. v1 differs in ways that matter: it names its filters wantedCollections / wantedDids, nests commit fields under a commit object, has no kinds filter, identifies events by a time_us timestamp instead of a seq, and offers no replay or snapshotting. Prefer v2 unless you are maintaining something already written against v1.

Filtering: ask for just your slice

Filtering happens server-side, so a narrow filter means less bandwidth and less work for your client. Three filters are available, and they combine:

  • collections: one or more NSIDs, or an app.bsky.feed.*-style namespace wildcard. Repeat the parameter for multiple collections.
  • dids: restrict the stream to specific accounts by DID. Repeat for multiple accounts.
  • kinds: which event kinds to receive — commit, identity, account, sync. Omit it for all four.
wss://jetstream.us-east.bsky.network/xrpc/network.bsky.jetstream.subscribeEvents
?collections=app.bsky.feed.post
&collections=app.bsky.feed.like
&dids=did:plc:eygmaihciaxprqvxpfvl6flk
&kinds=commit

A collection filter constrains commit events only — identity, account, and sync events flow regardless — so a commits-only stream needs kinds=commit as well.

Filter limits

A single subscription accepts up to 100 collections and 10,000 DIDs. Exceeding either is rejected before the WebSocket upgrade, so a client that builds its filter list dynamically should bound it rather than discover the cap as a failed connection.

Resuming where you left off

Every event carries a seq, a monotonically increasing sequence number. If your consumer drops, reconnect with ?cursor=N (the last value you processed) and Jetstream replays from there, within a bounded lookback window:

wss://jetstream.us-east.bsky.network/xrpc/network.bsky.jetstream.subscribeEvents?collections=app.bsky.feed.post&kinds=commit&cursor=12345

The cursor is inclusive — you'll receive event N itself again — and delivery is at-least-once, so an event may arrive more than once across a reconnect. Make your handlers idempotent. Key on each record's at:// URI, so duplicates are harmless.

For the live tail only, cursor also accepts a unix-microsecond timestamp, which the server recognizes by its magnitude and translates to the nearest seq. That's how you resume from a point in time rather than a specific event — and it's what lets a consumer written against v1's time_us keep working against a v2 host.

Getting history, not just live

When you also need historical data — e.g., every app.bsky.feed.post from the last month, then live updates from there — Jetstream replays the archive over HTTP and cuts over to this live tail at the tip. The server holds the archive, so there's nothing to stage on your side; your code never has to know whether a given event is history or real time.

This flow is called Network Replay. It uses the same filters as the live tail and adds a few authenticated HTTP calls to pull the history.

How Jetstream relates to the firehose

Under the hood, Jetstream consumes the network's full firehose from a Relay and does the decoding and filtering so you don't have to. You generally only need the raw firehose when you want full network proofs:

JetstreamRelay firehose
EncodingJSON, already decodedbinary (DAG-CBOR + CAR)
Filteringby collection and/or DIDnone; you get everything
Historylive, historical, or bothlive only
What you runa WebSocket clienta CBOR/CAR decoder, cursor logic

Use the raw firehose when you need every record with no filtering, or the cryptographic proofs to verify repository state yourself. For nearly everything else, Jetstream is the simpler path.

Running your own Jetstream

The public instances are enough for most consumers. If you'd rather run your own, Jetstream is a single static Go binary that archives the network to local disk and serves the same live tail, replay, and snapshot endpoints as the public instances. Running your own Jetstream covers what to expect: the initial backfill, hardware, configuration, and how to gate the expensive replay endpoints the way Bluesky's instances do.

See also