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

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 two sets of public instances. The live tail works the same on both, with one addition on v2: each event also carries a cursor field (see Resuming).

VersionEndpointRegion
v2wss://jetstream.us-west.bsky.networkUS West
v2wss://jetstream.us-east.bsky.networkUS East
v1wss://jetstream1.us-west.bsky.networkUS West
v1wss://jetstream2.us-east.bsky.networkUS East

Jetstream v2 is recommended for new projects. Replay and Snapshotting are only available on Jetstream v2. Jetstream v1 provides legacy timestamps.

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

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