Skip to content
SocialsForAI
Menu

Bluesky character counter

300 graphemes. Count them properly.

Bluesky counts what you see as one character, not code units. Bytes matter too.

Graphemes: 50 / 300 (250 left)

Bytes: 74 / 3,000 (2,926 left)

The short answer

A Bluesky character counter measures a post against Bluesky's two limits from the app.bsky.feed.post lexicon: 300 graphemes and 3,000 UTF-8 bytes. A grapheme is one character as a reader sees it, so a family emoji built from seven code points counts once. This counter shows both numbers live, because emoji-heavy posts can hit the byte limit first.

Graphemes, code points, UTF-16 and bytes

Text has at least four lengths, and different tools report different ones. A grapheme is what a reader sees as one character. A code point is one Unicode character number. JavaScript's .length counts UTF-16 code units, where characters outside the basic range take two. Bytes are the size of the text in UTF-8, the encoding Bluesky stores. These numbers come from running the counter's own functions:

  • "hello": 5 graphemes, 5 code points, 5 UTF-16 units, 5 bytes.
  • "cafΓ©" typed as one Γ©: 4 graphemes, 4 code points, 4 units, 5 bytes.
  • "cafΓ©" typed as e plus a combining accent: still 4 graphemes, but 5 code points, 5 units, 6 bytes.
  • "πŸ‘πŸ½" (thumbs-up with skin tone): 1 grapheme, 2 code points, 4 units, 8 bytes.
  • "πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦" (family): 1 grapheme, 7 code points, 11 units, 25 bytes.
  • "πŸ‡ΊπŸ‡Έ" (a flag): 1 grapheme, 2 code points, 4 units, 8 bytes.
  • "ζ—₯本θͺž": 3 graphemes, 3 code points, 3 units, 9 bytes.
  • "𝗕𝗼𝗹𝗱" (Unicode bold): 4 graphemes, 4 code points, 8 units, 16 bytes.

Two limits: 300 graphemes and 3,000 bytes

The post record's text field in Bluesky's app.bsky.feed.post lexicon has maxGraphemes: 300 and maxLength: 3000, where the length is in bytes. A post has to satisfy both. For most writing, the grapheme limit is the one you hit: 300 plain Latin letters are 300 bytes, and 300 Japanese characters are 900 bytes, both well under 3,000.

Emoji are where bytes catch up. A family emoji is 1 grapheme but 25 bytes, so 120 of them reach 3,000 bytes while using only 120 of the 300 graphemes. Three hundred of them would be 7,500 bytes. If a post is mostly emoji or flags, watch the byte meter rather than the grapheme one.

Bluesky's 300 is higher than X's 280, and the units differ. The sample in the box, "Family update πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ counts as one grapheme on Bluesky.", is 50 graphemes and 74 bytes here, and weighs 51 in the X character counter. More on the rules in our Bluesky character limit guide.

Alt text for images

Bluesky posts can carry up to 4 images, each up to 2,000,000 bytes, per the app.bsky.embed.images lexicon. Every image object has a required alt field, described there as "Alt text description of the image, for accessibility". The lexicon requires the field but allows it to be an empty string, so an image can be posted with no description at all.

Write one anyway. Alt text is what screen reader users get in place of the image, and it doesn't count toward the 300 graphemes of the post text. In our API, pass it as alt on each media item; if you leave it out we send an empty string, which is valid but tells a screen reader user nothing.

json
{
  "accounts": ["acc_..."],
  "text": "New docs for webhooks are up.",
  "media": [{ "url": "https://example.com/webhooks-diagram.png", "alt": "Diagram: a post is published, then a signed webhook is sent to your server." }]
}

What happens over the limit through our API

Bluesky is one of the five networks live on our API. Before anything is sent, our preflight check counts the text in graphemes, the same function this counter uses. Over 300, the request fails with HTTP 422, preflight_failed, and a text_too_long issue that says how much to cut. Nothing is published to any account in the request. Send dry_run: true to run the checks without posting.

The check covers both limits. A post under 300 graphemes but over 3,000 bytes, which takes a lot of emoji, also fails with text_too_long, and the message gives the byte count. The byte meter above shows the same number as you type.

json
{
  "error": {
    "code": "preflight_failed",
    "message": "The post would fail on 1 check(s). Nothing was published.",
    "fix": "Fix the issues below, or send dry_run: true to iterate without publishing.",
    "issues": [{
      "account_id": "acc_...",
      "platform": "bluesky",
      "severity": "error",
      "code": "text_too_long",
      "field": "platform_options.bluesky.text",
      "message": "Text is 312 characters; Bluesky allows 300.",
      "fix": "Shorten it by 12+ characters, or set platform_options.bluesky.text to a shorter version."
    }]
  }
}

Posting the same text to Bluesky, X and LinkedIn

The same draft lands differently on each network: 300 graphemes on Bluesky, 280 weighted characters on X, 3,000 characters on LinkedIn. A post that fits Bluesky with a long link may be over on X once its emoji double, or under on X because its link shrinks to 23. Use platform_options to give a network its own version of the text when one draft can't fit all three. Our cross-posting guide walks through it, and the docs list every field.

Questions

About this tool

What's the Bluesky character limit?

300 graphemes and 3,000 UTF-8 bytes, from the text field of Bluesky's app.bsky.feed.post lexicon. A post must satisfy both. Ordinary writing hits the 300 grapheme limit long before 3,000 bytes; posts packed with emoji can hit the byte limit first, which is why this counter shows both numbers.

What's a grapheme?

A grapheme is one character as a reader sees it, even when it is built from several Unicode code points. "πŸ‘πŸ½" is two code points but one grapheme, and the family emoji "πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦" is seven code points but one grapheme. Bluesky's 300 limit counts graphemes, so each of those counts once.

Why does an emoji count as one character on Bluesky but two on X?

The networks count differently. Bluesky counts graphemes, so any emoji, however complex, is 1. X uses weighted characters and its docs say every emoji counts 2. The same family emoji is 1 grapheme on Bluesky, 2 on X, and 25 bytes toward Bluesky's separate 3,000-byte limit.

Do links count toward Bluesky's 300 characters?

Yes, at their full length. Bluesky doesn't shorten links to a fixed weight the way X does with t.co. "https://example.com/docs/webhooks" counts 33 graphemes on Bluesky and 23 on X. The link itself is stored as a facet, a byte range over the text, which makes it clickable.

Does alt text count toward the 300 limit?

No. Alt text is stored on each image in the app.bsky.embed.images record, separately from the post text, so it doesn't use any of the 300 graphemes. The lexicon requires an alt field on every image but allows an empty string. Write a real description; it is what screen reader users get instead of the image.

What happens if I post more than 300 graphemes through the API?

Our API rejects it before posting, with HTTP 422 preflight_failed and a text_too_long issue that names the length, the limit and how much to cut. Nothing is published. The check covers both limits, so a post under 300 graphemes but over 3,000 bytes is caught the same way.

Posting from code or an agent? SocialsForAI runs the same checks before every post. Bluesky character limit Β· Bluesky API Β· X character counter Β· Cross-posting

Get an API key and send your first post in five minutes.