A container ID is a receipt, not a successful upload. The Instagram Graph API takes your video, hands back a container ID immediately, and does the real work afterwards — which is why the field that tells you whether the upload actually worked lives on a different endpoint, and why status_code is the only thing worth reading when a post silently fails to appear.
Everything below is the API publishing channel documented on developers.facebook.com. It is not the same set of rules as uploading from the Instagram app, and the numbers do not transfer between the two.
What status_code actually tells you
Meta states the asynchrony plainly:
"Video uploads are asynchronous, so receiving a container ID does not guarantee that the upload was successful. To verify that a video has been uploaded, request the status_code field on the IG Container. If its value is FINISHED, the video was uploaded successfully."
If your integration treats a successful container-creation response as "uploaded," it will report success for videos that never made it through. That is the mechanism behind the most frustrating version of this bug: a scheduler showing a green check for a post that does not exist on the account.
The Content Publishing docs say exactly when to reach for the field:
"If you are able to create a container for a video but the POST
/<IG_ID>/media_publishendpoint does not return the published media ID, you can get the container's publishing status by querying the GET/<IG_CONTAINER_ID>?fields=status_codeendpoint. This endpoint will return one of the following:"
The five values, in Meta's own words
| Value | Official meaning |
|---|---|
EXPIRED | "The container was not published within 24 hours and has expired." |
ERROR | "The container failed to complete the publishing process." |
FINISHED | "The container and its media object are ready to be published." |
IN_PROGRESS | "The container is still in the publishing process." |
PUBLISHED | "The container's media object has been published." |
Two things people consistently misread in that list:
- FINISHED does not mean your post is live. Meta's wording is "ready to be published" — it is the green light to call
media_publish, not the result of that call.PUBLISHEDis the separate value that means the media object went out. - IN_PROGRESS is not a failure. It is the expected state while Meta processes the file. Treating it as an error and re-uploading creates a second container for the same video, which costs you against a limit covered further down.
EXPIRED and ERROR are the two that require action, and they point at completely different problems — one is a clock, the other is the file.
How often to poll
Meta gives a specific cadence, and it is worth following literally rather than inventing your own backoff:
"We recommend querying a container's status once per minute, for no more than 5 minutes."
Once per minute, capped at five minutes. The recommendation stops at the five-minute mark — for anything you want to do past that point, check the official documentation rather than inventing a policy.
The fastest fix: make the file match the published API spec
ERROR only tells you the container "failed to complete the publishing process" — the documented meaning stops there. So the actionable move is to check your file against the Reel specification Meta publishes for the API channel, line by line:
"Container: MOV or MP4 (MPEG-4 Part 14), no edit lists, moov atom at the front of the file." "Audio codec: AAC, 48khz sample rate maximum, 1 or 2 channels (mono or stereo)." "Video codec: HEVC or H264, progressive scan, closed GOP, 4:2:0 chroma subsampling." "Frame rate: 23-60 FPS." "Video bitrate: VBR, 25Mbps maximum" "Audio bitrate: 128kbps" "Duration: 15 mins maximum, 3 seconds minimum" "File size: 300MB maximum"
The spec also caps horizontal pixels at 1920 and states the required aspect ratio is "between 0.01:1 and 10:1 but we recommend 9:16 to avoid cropping or blank space."
The first line is the one that bites hardest. "No edit lists, moov atom at the front of the file" is a container-layout requirement, not a codec requirement — a file can be an H.264 MP4 with a perfect bitrate and still carry its moov atom at the end. It plays fine on your machine, passes every visual check you can think of, and still fails on the API side.
Re-muxing with the moov atom moved to the front is the cheapest thing to try, and it does not require re-encoding the video. In FFmpeg that is -movflags +faststart; the same container fix is covered in more depth in FFmpeg faststart and the moov atom.
Try it right here
Runs in your browser. Your file is not uploaded anywhere.
The converter above runs in your browser tab — the file is read from disk, processed locally, and handed back, so nothing is uploaded on the way to fixing an upload. For a QuickTime file straight off a Mac or an iPhone, MOV to MP4 is the direct route.
If the file is over the API's 300MB ceiling or above the 25Mbps VBR cap, that is a bitrate problem rather than a container problem — compress the video first, then re-check the layout.
EXPIRED is a clock problem, not a file problem
The documentation is blunt about container lifetime:
"Containers expire after 24 hours"
So a container created by a scheduler that queues posts a day or more in advance can be valid at creation and dead by the time the publish call runs. The fix is not to re-encode anything — it is to create the container closer to the publish moment.
Two different 24-hour gates, and they are not the same number
This is where a lot of integrations get their arithmetic wrong. There are two separate limits, on two different actions, with two different numbers.
Creating containers:
"An Instagram account can only create 400 containers within a rolling 24 hour period"
Publishing posts:
"Instagram accounts are limited to 100 API-published posts within a 24-hour moving period. Carousels count as a single post."
These are not two descriptions of one quota. One counts containers you create, the other counts posts you publish. A carousel is assembled from child containers — the children parameter takes "an array of up to 10 container IDs" — yet the whole carousel counts as a single post against the publishing limit. Every retry after an ERROR creates another container too, and those count against the 400 the same way.
Other things to rule out before blaming the video
- Images must be JPEG. "JPEG is the only image format supported. Extended JPEG formats such as MPO and JPS are not supported." A PNG cover or child image is a documented non-starter, not a transient failure.
- Reels cannot go in carousels. The docs state it directly: "Reels cannot appear in carousels." If your workflow is trying to bundle one in, the design is the problem.
- Caption limits. "Maximum 2200 characters, 30 hashtags, and 20 @ tags." Auto-generated captions with a hashtag block are the usual way to exceed these without noticing.
- A published Reel missing from the Reels tab is not an upload failure. On
share_to_feed, Meta notes that "neither value determines whether the reel actually appears in the Reels tab because the reel may not meet eligibilty requirements or may not be selected by our algorithm." (The spelling is Meta's own.) That is a distribution outcome, not astatus_codeproblem.
How to tell it worked
Query the container once per minute. FINISHED means you may now call media_publish; a media ID back from that call, or a subsequent PUBLISHED status, means the media object is out. ERROR and EXPIRED point at the file spec and the 24-hour clock respectively — neither is a reason to retry blindly into your container budget.
FAQ
What does status_code ERROR mean on Instagram's API? Officially, only this: "The container failed to complete the publishing process." The documented meaning stops there, so the practical next step is to check the file against the published Reel specification — container layout, codec, frame rate, duration, bitrate and the 300MB ceiling, all of which apply to the API channel.
How often should I poll the container status? Meta's recommendation is "querying a container's status once per minute, for no more than 5 minutes." More frequent polling buys you nothing the documentation offers.
Why did my container expire before I could publish it? Because "containers expire after 24 hours." If you create containers far ahead of the scheduled publish time, they can die in the queue. Create them closer to when you intend to publish.
Is the 400-container limit the same as the 100-post limit? No. They are two independent gates: 400 containers created within a rolling 24 hour period, and 100 API-published posts within a 24-hour moving period. Carousels count as a single published post but are built from up to 10 child containers, so the two counters move at different speeds.
My MP4 plays fine locally — why does the API reject it? The API spec requires "no edit lists, moov atom at the front of the file." Local players do not care where the moov atom sits, so a file can be perfectly playable and still fail this check. Re-muxing with the atom moved to the front resolves it without re-encoding.
Does the API's 300MB limit apply to uploads from the Instagram app? No. 300MB is the API channel's stated maximum. Instagram publishes no equivalent figure for in-app uploads — see the Instagram Reels file size limit for why the numbers circulating for that case mostly come from the wrong channel.
Specs and limits quoted from Meta's IG User Media and Content Publishing documentation, API channel only. More upload-error walkthroughs in the fix archive, and the browser-based converters are listed under all Clapr tools.