Graze
    MarketplaceHelpBlogAboutLogin
    Docs
    • Introduction
    • Creating Your First Feed
    • Nodes for Beginners
    • Custom Sorting
    • Testing and Fixing your Feed
    • Debugging a Post
    Guides
    • Dashboard Overview
    • Page Layers and Audiences
    • Using AI Nodes
    • Natural Language and Ask a Question Nodes
    • Feed Gym
    • Feed Feedback and Personalization
    • Content Moderation
    • Sticky Posts
    • Subscription Lists
    • Graze For Communities
    • Collaborators
    • Curation Desk
    • Feed Pulse
    • Version History
    • Using Custom Nodes
    • Migrating a Feed
    • Sponsored Posts
    • Embedding Feeds
    API
    • Editor Documentation
    • Ratio Comparison Node
    • Rate Limits
    • Custom Node API
    • Contrails
    • Graze Turbostream
    • API Documentation: Privacy Compliance Endpoints
    Help
    • Frequently Asked Questions
    • Feature Requests
    • Marketplace
    Connecting

    Graze Turbostream

    A live WebSocket stream of new Bluesky posts, each one arriving with the author's profile and the posts it replies to or quotes already filled in.

    Turbostream is a live stream of new posts on Bluesky. It's built on Bluesky's Jetstream, but each post arrives with its context already filled in: the author's profile, the profiles of anyone mentioned, and the posts it replies to or quotes.

    A raw Jetstream event only gives you identifiers (DIDs and AT-URIs). To make sense of it you'd normally look each one up through the Bluesky API, and cache the results. Turbostream does that work for you, so you can read the stream and use it straight away.

    Connecting

    There's a single WebSocket endpoint and no setup:

    wss://api.graze.social/app/api/v1/turbostream/turbostream

    From the command line:

    websocat "wss://api.graze.social/app/api/v1/turbostream/turbostream"

    From JavaScript:

    const ws = new WebSocket("wss://api.graze.social/app/api/v1/turbostream/turbostream");
    ws.onmessage = (event) => {
      const msg = JSON.parse(event.data);
      if (msg.pong) return; // heartbeat
      console.log(msg.at_uri, msg.hydrated_metadata.user?.handle);
    };

    What you receive

    Each WebSocket message is one JSON object: a post, or a heartbeat.

    Posts

    {
      "at_uri": "at://did:plc:qipvtyo27owt4isjah3j3dw2/app.bsky.feed.post/3lng7e4mr5c2z",
      "did": "did:plc:qipvtyo27owt4isjah3j3dw2",
      "time_us": 1745342977546824,
      "message": { "did": "…", "time_us": 1745342977546824, "kind": "commit", "commit": { "…": "…" } },
      "hydrated_metadata": {
        "user": { "handle": "…", "displayName": "…", "followersCount": 0 },
        "mentions": {},
        "parent_post": null,
        "reply_post": null,
        "quote_post": null
      },
      "_id": "1745342977612-0"
    }
    • at_uri: the post's address.
    • did: the author's DID.
    • time_us: when Jetstream received the post, in microseconds.
    • message: the original Jetstream event, unchanged. The post itself is in message.commit.record.
    • hydrated_metadata: the context Graze filled in (see below).
    • _id: this message's position in the stream.

    What's filled in

    • user: the author's full profile, including handle, display name, avatar, bio and follower counts.
    • mentions: everyone mentioned in the post, as a map from their DID to their profile.
    • parent_post: the post being replied to, if the post is a reply.
    • reply_post: the first post of the thread, if the post is a reply.
    • quote_post: the quoted post, if the post quotes one.

    Any of these is null (or, for mentions, empty) when it doesn't apply or couldn't be fetched in time. Always check before using them.

    Replays and heartbeats

    • On connect, you first receive the 25 most recent posts, each marked "_replay": true. Then live posts follow.
    • Every 10 seconds, you receive {"pong": true}. Use it to check the connection is alive.

    You may see other fields in a message. Ignore any you don't recognise.

    No resume

    Turbostream has no cursor. If you disconnect, any posts published while you were away are not sent when you reconnect; you only get the latest 25 again, as a replay. If you need every post, keep the connection healthy and reconnect quickly.

    Privacy

    Accounts that have asked to be left out of Graze don't appear in Turbostream. Their posts aren't sent, and they're left out of mentions, parent_post, reply_post and quote_post on other people's posts.

    Tips for your client

    • Run a watchdog. If nothing (not even a heartbeat) arrives for about 25 seconds, close the connection and reconnect. A connection can go silent without closing.
    • Reconnect with backoff. If reconnecting fails, wait a little longer before each try.
    • Skip or handle replays. If you count or compare posts, filter out _replay messages first, or you'll double-count the 25 you already saw.
    • Keep up. Turbostream carries a lot of posts. Do slow work (database writes, API calls) off the receive loop, so you don't fall behind.
    • Read time from the event. If time_us at the top level is null, use message.time_us.

    Looking for only some posts?

    Turbostream sends every new post. If you only want posts that match a topic, build a feed in Graze and use Contrails to stream just its matches.

    • Connecting
    • What you receive
    • Posts
    • What's filled in
    • Replays and heartbeats
    • No resume
    • Privacy
    • Tips for your client
    • Looking for only some posts?
    Stay in the know
    Follow on BlueSkyJoin the Discorddiscord_fillSubscribe to the newsletter
    Company
    AboutBlogPressContact
    Support
    Community GuidelinesDocsSite StatusHelp CenterFeature Requests
    Legal
    Privacy PolicyTerms & Conditions
    © 2026 Graze.social. All rights reserved.