Cover for Bluesky API Limits: Post, Image, Video, and Rate Limits (2026)

Bluesky API Limits: Post, Image, Video, and Rate Limits (2026)

Complete reference for Bluesky API limits — 300 character posts, 1 MB images, 100 MB / 3 min videos, 5,000 points/hour rate limits. All from official sources.

Apr 16, 2026 · AlexandroAlexandro
BlueskyAPI ReferenceSocial Media Development
Table of Contents

Bluesky posts are capped at 300 graphemes of text, 4 images (1 MB each), or 1 video (100 MB, 3 minutes). Rate limits allow up to 1,666 post creations per hour per account. Here's every limit you need to know, pulled from the official AT Protocol lexicons and Bluesky documentation.

Bluesky API limits documentation reference

Text limits

LimitValueSource
Max graphemes300app.bsky.feed.post lexicon
Max bytes (UTF-8)3,000app.bsky.feed.post lexicon
Text required?No — empty string allowed if embed is presentapp.bsky.feed.post lexicon
Rich textMentions, links, hashtags via facetsapp.bsky.richtext.facet

The 300-grapheme limit is what most people think of as "300 characters," but it's more nuanced. A Latin character is 1 grapheme. An emoji like 👨‍👩‍👧‍👧 is also 1 grapheme, but it's 25 bytes in UTF-8. You'll hit the byte limit before the grapheme limit if your post is emoji-heavy.

The @atproto/api SDK's RichText class handles grapheme counting and facet detection. If you're building a client, use it instead of String.length (which counts UTF-16 code units, not graphemes).

Image limits

LimitValueSource
Max images per post4app.bsky.embed.images lexicon
Max file size1 MB (1,000,000 bytes)app.bsky.embed.images lexicon
Accepted formatsJPEG, PNG, WebPapp.bsky.embed.images lexicon
Alt textOptional, max 1,000 graphemesapp.bsky.embed.images lexicon
Aspect ratioOptional but recommendedapp.bsky.embed.defs#aspectRatio

Images are uploaded as blobs via com.atproto.repo.uploadBlob, which returns a blob reference you embed in the post record. The 1 MB limit is enforced by the PDS.

If you don't know the aspect ratio, leave the aspectRatio field undefined rather than guessing. Clients will handle it, but wrong ratios cause layout jank.

Video limits

LimitValueSource
Max videos per post1app.bsky.embed.video lexicon
Max file size100 MBapp.bsky.embed.video lexicon
Max duration3 minutesServer-side enforcement
Accepted formatMP4 onlyapp.bsky.embed.video lexicon
Alt textOptional, max 1,000 graphemesapp.bsky.embed.video lexicon
CaptionsUp to 20 VTT filesapp.bsky.embed.video lexicon
Daily limit25 videos or 10 GB totalBluesky blog (Sep 2024)
Email verificationRequired before uploadingBluesky docs

These limits have changed over time:

DateMax sizeMax duration
September 2024 (launch)50 MB60 seconds
March 2025 (v1.99)100 MB3 minutes

How video upload works

There are two ways to upload video via the API:

Simple method — upload the file as a blob to your PDS using uploadBlob, then reference it in the post. The downside: the video only starts processing after the post is published, so followers see a broken video for several seconds.

Recommended method — upload directly to video.bsky.app with a service auth token, poll getJobStatus until processing completes, then use the returned blob reference in your post. The video is ready to play the moment the post appears.

// 1. Get a service auth token
const { data: serviceAuth } = await agent.com.atproto.server.getServiceAuth({
  aud: `did:web:${agent.dispatchUrl.host}`,
  lxm: 'com.atproto.repo.uploadBlob',
  exp: Math.floor(Date.now() / 1000) + 60 * 30,
});

// 2. Upload to the video service
const uploadUrl = new URL(
  'https://video.bsky.app/xrpc/app.bsky.video.uploadVideo'
);
uploadUrl.searchParams.append('did', agent.session.did);
uploadUrl.searchParams.append('name', 'video.mp4');

const uploadResponse = await fetch(uploadUrl, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${serviceAuth.token}`,
    'Content-Type': 'video/mp4',
  },
  body: videoBytes,
});

const jobStatus = await uploadResponse.json();

// 3. Poll until processing is done
let blob = jobStatus.blob;
const videoAgent = new AtpAgent({ service: 'https://video.bsky.app' });

while (!blob) {
  await new Promise((r) => setTimeout(r, 1000));
  const { data } = await videoAgent.app.bsky.video.getJobStatus({
    jobId: jobStatus.jobId,
  });
  blob = data.jobStatus.blob;
}

// 4. Create the post with the processed video
await agent.post({
  text: 'Check this out',
  embed: {
    $type: 'app.bsky.embed.video',
    video: blob,
  },
});

Bluesky video upload API code example

Rate limits

Bluesky rate limits are per account, not per app or API key. If you're a tool that posts on behalf of multiple users, each user has their own limits.

LimitValueScope
Write operations5,000 points/hour, 35,000 points/dayPer account (DID)
Post creation cost3 pointsPer record
Max posts/hour~1,666Derived from points
Max posts/day~11,666Derived from points
HTTP API requests3,000 per 5 minutesPer IP
Session creation30 per 5 min, 300 per dayPer account

The points system covers all record operations, not just posts. Likes, follows, and reposts also cost points (3 for create, 2 for update, 1 for delete).

For most apps, you'll hit the 3,000 requests/5 min IP limit before the per-account write limits. If you're posting for hundreds of users from a single server, space out your API calls.

What you can't do

A few things the Bluesky API doesn't support or handles differently than you'd expect:

  • No GIF animation. GIF files upload fine but display as static images. Use MP4 with presentation: "gif" for looping short clips.
  • No mixing images and video. A post embed is either app.bsky.embed.images or app.bsky.embed.video, not both.
  • No server-side link previews. Unlike Twitter or Facebook, Bluesky doesn't auto-generate link cards. Your client must fetch OG metadata and upload the thumbnail as a blob using app.bsky.embed.external.
  • No edit. Posts are immutable records. Delete and re-create.
  • No scheduled posts. The API creates posts immediately. Scheduling is your app's responsibility.

Posting to Bluesky the easy way

If you're building a social media tool or just want to post to Bluesky alongside other platforms without dealing with facet parsing, blob uploads, and video processing polling — PublishQ handles all of it.

Write your post once, attach media, and publish to Bluesky, Twitter, Instagram, Facebook, LinkedIn, Threads, TikTok, and YouTube from one API call. PublishQ takes care of rich text detection, image dimension extraction, video preprocessing, and per-platform formatting.

You can also do it from the dashboard UI without writing any code.

For the full list of what's supported on Bluesky, see the Bluesky platform docs.

curl -X POST https://publishq.com/api/v1/posts \
  -H "Authorization: Bearer pq_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Hello Bluesky! 🦋",
    "mediaIds": ["media-abc123"],
    "accounts": [
      { "accountId": "bluesky-account-id" },
      { "accountId": "twitter-account-id" },
      { "accountId": "threads-account-id" }
    ],
    "publishNow": true
  }'

Bluesky's 300-character limit is tighter than most platforms. If your main content is longer, use postOverrides to send a shorter version just to Bluesky while keeping the full text for everything else:

curl -X POST https://publishq.com/api/v1/posts \
  -H "Authorization: Bearer pq_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "content": "We just shipped a huge update to our analytics dashboard. Here is everything that changed and why we made these decisions...",
    "accounts": [
      { "accountId": "linkedin-account-id" },
      { "accountId": "facebook-account-id" },
      {
        "accountId": "bluesky-account-id",
        "postOverrides": {
          "content": "Shipped a huge analytics update today 📊🦋"
        }
      }
    ],
    "publishNow": true
  }'

One request. Three platforms. No blob management. No character-limit headaches.

Start posting to Bluesky with PublishQ →

Publish to Bluesky and multiple platforms with one API call

Frequently Asked Questions

Direct answers extracted from this article.

Bluesky posts have a 300 grapheme limit (roughly 300 characters for Latin text). The underlying byte limit is 3,000 bytes UTF-8. Emojis count as 1 grapheme but may use 4+ bytes.
Bluesky videos can be up to 100 MB and 3 minutes long. Only MP4 format is accepted. This was increased from 50 MB / 60 seconds in March 2025.
Up to 4 images per post. Each image must be under 1 MB (1,000,000 bytes). Supported formats are JPEG, PNG, and WebP.
Bluesky rate limits are per account: 5,000 points per hour and 35,000 per day. A post creation costs 3 points, so you can create up to 1,666 posts per hour. HTTP API requests are limited to 3,000 per 5 minutes per IP.
Bluesky accepts GIF files as image uploads, but only displays the first frame as a static image. For animated content, convert your GIF to MP4 and upload it as a video.
Yes. Upload the MP4 file to video.bsky.app using a service auth token, poll for processing completion, then embed the returned blob reference in your post record.
Start for Free

No credit card required • Set up in under 3 minutes