Skip to main content

Research · verified 26 August 2026

Social media publishing limits, and what the API docs get wrong

We publish to nine networks, so we had to write a validator for 41 separate limits. Most of them are the documented numbers. Two are not, and the four findings below are the reasons posts fail after they look fine in the editor.

By Tayyab, Founder · Published

What we stop before the network does

These are the 41 limits our own validator checks a post against before it leaves the composer, which is why a post that would be rejected fails here, with the reason, rather than at a network's API an hour after you scheduled it. They run from 280 characters on X (Twitter) to 63,206 on Facebook, a spread of 226x, which is the whole reason one draft cannot go to nine networks unedited.

Pre-publish limits Content Drifter enforces on each network
NetworkCharactersHashtagsImagesVideoMedia required
Instagram2,200301015 minYes
TikTok2,2005video only10 minYes
X (Twitter)280542m 20sNo
Facebook63,206101020 minNo
LinkedIn3,00052030 minNo
Threads5005205 minNo
YouTube5,00015video only12hYes
Pinterest8005115 minYes
Bluesky300 graphemes541 minNo

A post with no image or video is refused outright on Instagram, TikTok, YouTube, Pinterest. Threads (5) is the one network where we also cap how many distinct URLs the body can carry. Bluesky counts graphemes rather than characters, so an emoji costs one, not four.

What this table is not: a mirror of every rule each network applies. It is the set we check first, and a network can tighten a limit without announcing it. When one of ours turns out to be wrong, the publish attempt still fails and the error comes back to the post, rather than the post being marked sent.

Four things the documentation does not tell you

Bluesky’s 300 is graphemes, not characters

A JavaScript string length is not what the AT Protocol counts. An emoji assembled from several code points, or a letter with a combining mark, is one grapheme to Bluesky and four or more to String.length. A post measures 298 in your editor, passes your validator, and is rejected at the protocol boundary, which is the worst place to find out because the user has already scheduled it.

We count with Intl.Segmenter before anything is sent, and fall back to code-point length where it is unavailable, which is slightly conservative for emoji that join with zero-width joiners. Conservative is the correct direction to be wrong in here.

Threads counts unique URLs, not links

Threads has capped links at five since December 2025, and the cap is on DISTINCT urls. Meta's THREADS_API__LINK_LIMIT_EXCEEDED counts unique URLs rather than link occurrences, so the same link repeated six times is one URL and passes, while six different links fail. A validator that counts matches rejects valid posts; one that counts links without normalising case lets invalid ones through.

We dedupe case-insensitively, match bare www. URLs as well as full ones, and stop at punctuation that is usually a sentence boundary while keeping trailing slashes, query strings and fragments inside the URL.

Truncating to a limit produces valid posts nobody meant to write

Fitting text to a character cap is where most tools stop thinking, and it is where the worst output comes from. Trim a 400-character draft that opens "Hi." down to X's limit by cutting at the last sentence end and you get a post that reads "Hi.". It is a complete sentence. It passes every check. It is not the post the user wrote.

So our fitter returns how much of the original survived alongside the text, and a caller can refuse to ship a stub rather than publishing something technically valid. Completeness is not the useful signal; the ratio is.

The one we missed for 52 days

Every rule above is one we enforce. Here is one we did not. Our publish pipeline marks a row publishing, calls the platform, and settles it to success or failed. If the process dies between those two steps, or the provider drops the request, nothing settles it, because there was no timeout anywhere.

On 1 August 2026 we found two rows in that state on production. One had been there for 52 days. The row was not the cost. The cost was that the calendar rendered "Publishing…" with a spinner indefinitely, so the user believed work was in progress that had stopped seven weeks earlier; that the retry control only picks up failed rows, so the one recovery available could not see it; and that every count of how many publishes failed was quietly wrong.

A wedged row is failed, not lost. A reaper now flips it with an explicit reason, which puts it back within reach of the retry path that already existed rather than inventing a new one. If you are building this, put a timeout on the transient state before you need one.

How to use this, and how not to

Every number in the table is generated from the object our publish path reads at runtime, so it cannot describe rules we do not run, and it changes in the same commit our enforcement changes. The dated incidents are literals, because what happened on a date does not move.

What it is not: a mirror of every rule each network applies. These are the checks we run first, chosen because they are the ones we watched fail. A network can tighten a limit without telling anyone, and when one of ours turns out to be wrong the publish still fails and the error comes back to the post rather than the post being marked sent. Cite it as one team's enforced ruleset with a verification date, which is what it is.

FAQ

Frequently asked questions

What is the character limit on each social network?

X is the tightest at 280 characters and Facebook the loosest at 63,206, with Bluesky at 300 and LinkedIn at 3,000 in between. The full table is below and it is generated from the rules our publisher runs, not typed by hand, so it moves when our enforcement moves. Verified 26 August 2026.

Why does my Bluesky post fail when it is under 300 characters?

Because Bluesky counts graphemes, not characters, and a JavaScript string length is neither. An emoji built from several code points is one grapheme to Bluesky and four or more to String.length, so a post can measure 298 in your editor and be rejected at the AT Protocol boundary. We hit this, and we now count with Intl.Segmenter before anything is sent.

How many links can a Threads post contain?

Five unique URLs, a cap Threads introduced in December 2025. The word unique is the part that catches people out: Meta’s THREADS_API__LINK_LIMIT_EXCEEDED counts distinct URLs rather than link occurrences, so the same link six times is one URL and passes, while six different links fail. We dedupe case-insensitively before counting.

Which networks refuse a post with no image or video?

Instagram, TikTok, YouTube, Pinterest reject a text-only post outright, so we refuse it in the composer rather than letting it fail at the API. Two of those four take no still image at all and want video: a text post and a photo post both fail there for the same reason.

Is this table the complete set of rules each network applies?

No, and treating it as one will cost you. These are the checks OUR publisher runs before a post leaves the composer, chosen because they are the ones we saw fail in production. A network can tighten a limit without announcing it, which is how we ended up writing most of this page.