You have one image to post to Instagram, Threads, and Bluesky. Two of those belong to the same company. It sounds like it should be easy.
Start with the file format. Instagram's docs say only JPEG is supported. Threads officially supports both JPEG and PNG. Same company, different rules — the exact same PNG uploads to Threads and gets rejected by Instagram.
Carousels are another one. Instagram allows up to 10 items; Threads allows up to 20, with a minimum of 2. Again: same company, same feature, different limits.
Then there are file sizes. Threads accepts images up to 8 MB. Bluesky's limit is exactly 1,000,000 bytes — not "about a megabyte," their own sample code rejects anything larger than 1000000. A 2.5 MB photo works on two platforms and fails on the third.
The bigger difference, though, isn't the limits. It's how the platforms expect images to reach them.
Meta doesn't want your file. It wants a URL.
You put the image somewhere publicly accessible, send image_url, and, as the docs put it, "we will cURL your image using the URL provided." So before you can publish an image to Instagram, you need somewhere to host it.
Bluesky works the other way around. You upload the bytes with uploadBlob, get back a blob reference, then include that reference in your post. The post never contains the image itself, it only points to it. Same idea, completely different model.
Meta also splits publishing into two steps: first you create a media container, then you publish it. The container expires after 24 hours, and if you miss the window you start over. Instagram recommends polling its status once a minute for up to five minutes. Threads suggests waiting about thirty seconds before publishing.
None of that shows up in the "post in three lines of code" examples. Your scheduler still has to deal with it.
Reels add another surprise. The upload endpoint isn't graph.facebook.com, it's rupload.facebook.com. Different hostname, same feature.
Bluesky has its own rules. Every image needs alt text, and if you don't have a description, you still send an empty string. It feels pedantic, but that's the API. The docs also recommend stripping EXIF data before upload — the server may enforce it later, but today it's still your responsibility.
One detail made me smile. Threads counts emojis against the 500-character limit by their UTF-8 byte length, so one 👋 counts as four. I'd already run into byte offsets while writing about Bluesky facets and assumed it was one of those open-protocol quirks. Apparently not.
Bytes come for everyone.
While we're talking about documentation, Instagram's own content publishing page says API-published posts are limited to 100 per rolling 24 hours. Further down the same page, in the carousel section, it says 50. Both numbers are in the official documentation.
At Publora we eventually ended up exposing one POST with content, platforms, and scheduledTime. For media you either provide mediaUrls — up to ten public HTTPS URLs in the same request, fetched server-side much like Meta does — or, if you're uploading local files, create a draft first, upload to a pre-signed S3 URL, then schedule.
If I weren't working on Publora, my advice would still be the same. If you're integrating with one network, use its native API: they're well documented, and you'll understand how the platform actually works.
Once you're supporting multiple platforms, the hard part stops being the HTTP requests. It's everything each platform quietly means when it says "upload an image."
Further reading
These are the primary sources for everything above.
- Instagram Content Publishing — containers, statuses, rate limits
- Threads Posts — media specifications, carousels, the emoji-byte note
- Posting via the Bluesky API — blobs, facets, alt text
Every limit and behavior above comes from the platforms' own documentation. Where the docs disagree with themselves, I've pointed it out.
Top comments (0)