yeetpostyeetpost

Bluesky API rate limits

October 8, 2026

The Bluesky API limits reads and writes by two different mechanisms, and both are published as concrete numbers, which puts it ahead of most social APIs. Requests are capped per IP address, at 3,000 per 5 minutes. Writes to a repository are capped per account, in points, at 5,000 per hour and 35,000 per day. You can be comfortably inside one and blocked by the other, and the two need separate handling in code.

#The points budget for repo writes

The rate limits page for Bluesky protocol services sets the write budget per account, keyed to the DID. Every content write costs points:

Against a budget of 5,000 points per hour and 35,000 points per day, the documentation works out the ceiling as at most 1,666 records created per hour and 11,666 per day.

Two things fall out of the pricing that are easy to miss. A post is not one unit of budget. A thread of ten posts is ten creates, which is 30 points, and if you also create a like and a repost record on each of them you have tripled that. Follow, like, repost and block are all records, so a follow-back script is spending from exactly the same budget as your posting.

The other is that editing beats replacing. An update costs 2 points, while deleting and recreating costs 1 plus 3, which is 4. If you maintain a record that changes often, a pinned status post or a generated feed entry, updating it in place is half the cost of the delete-and-repost pattern most people reach for first.

The daily budget is seven times the hourly one, so it is the hourly window that binds on any bulk job. A 5,000 record migration is 15,000 points, well inside the daily 35,000, but at 1,666 creates per hour it needs at least three hours of wall clock time no matter how fast your code is. Plan bulk imports as multi-hour jobs with resumable state, and expect the process to outlive the terminal window you started it in.

#Per-IP and per-endpoint limits

Reads and everything else are capped by IP address on the hosting service. The overall limit for a hosted account's PDS is 3,000 requests per 5 minutes. That works out to 10 requests per second sustained, which is generous for a bot and tight for anything running many accounts behind one address. Shared hosting, a NAT gateway, or a CI runner pool are the usual ways to hit this without doing anything abusive, and the fix is spreading egress across addresses.

Several endpoints carry their own tighter limits, listed on the same page:

The session limit is the one that bites real integrations. At 300 sessions per day per account, a worker that logs in fresh on every job, or on every container restart, will run out. Sessions are refreshable, so cache the session and use the refresh token, and call createSession only when the refresh fails.

The firehose carries its own ceiling of 50 events per second, 2,600 per hour and 21,000 per day. Labeling services are capped at 5 per second, 10,000 per hour and 100,000 per day. Blob uploads are capped by size, at 52,428,800 bytes, which is 50 MB.

#The 429 and what comes back with it

Crossing any of these returns HTTP 429. The AT Protocol XRPC specification defines 429 as a signal that a resource limit has been exceeded and that the client should back off, and says there may be a Retry-After header indicating a specific back-off period. Because it may be present, a client that reads only Retry-After will sometimes get nothing back and has to fall back on a constant.

Bluesky's own documentation says its services return rate limit headers on responses that developers can use to understand the current limits, without listing the field names. Those names have moved over the life of the IETF work they follow, so read whatever the response carries at runtime and log an unrecognised header.

The points budget is the awkward one. You have to meter that side yourself.

#A write scheduler that stays inside the budget

The working pattern is one queue per account with a local points meter, and a separate IP-level limiter shared by every account on the host.

POINTS = { create: 3, update: 2, delete: 1 }

enqueue(did, op):
    queue[did].push(op)

worker(did):
    meter = pointsMeter(did)      # sliding windows: 5000/1h, 35000/24h
    loop:
        op = queue[did].peek()
        cost = POINTS[op.kind]

        if not meter.canSpend(cost):
            sleep(meter.timeUntilAffordable(cost))
            continue

        acquire(ipLimiter)        # 3000 per 5 minutes, shared per host
        res = xrpc(op)

        if res.status == 429:
            sleep(res.headers["Retry-After"] or 60)
            continue              # do not pop, do not count the spend

        queue[did].pop()
        meter.spend(cost)

Keep the meter as two sliding windows, otherwise a job that starts near the end of an hour spends two budgets in a few minutes and stalls. Do not count a spend on a failed request, and do not pop the operation, so a 429 resumes exactly where it stopped. Make each operation idempotent by carrying a stable record key, because a timeout is ambiguous and a blind retry is how backfills end up with duplicates. And keep the session cached across restarts, since the 300 per day session limit is the constraint most likely to break a worker that is otherwise perfectly polite.

One practical note if you are following older links. The rate limits page that used to sit under the app documentation now redirects to the protocol services documentation named above, which is the current canonical location. The numbers here are what that page states in September 2026, and published limits do move, so keep them in configuration rather than in your retry logic.