Fix "moov atom not found" on Uploaded Videos in Your Pipeline

Your worker pulls a video someone uploaded, hands it to FFmpeg, and gets back moov atom not found followed by Invalid data found when processing input. The file plays fine when you download it to your laptop. Nothing about the input looks broken, and every search result for that error string sells data-recovery software for corrupted memory cards.
Quick answer: The FFmpeg error "moov atom not found" means the MP4 or MOV file being opened has nomoovbox in the bytes that were actually read, which usually means the upload was truncated before the trailingmoovwas written, or themoovsits at the end of the file and your reader fetched only part of it with a Range request. Compare the file's byte length to the source'sContent-Length, and if all the bytes are there, remux withffmpeg -i input.mp4 -c copy -movflags +faststart output.mp4so the index sits at the front; if the bytes are missing, re-fetch or reject the upload with a useful error. FFmpeg Micro runs that same validate-and-remux step as one API call, with no encoder to host and no partial download to babysit.
What the moov atom actually is, and why it goes missing
The moov atom is the index of an MP4 or QuickTime file: track list, codec parameters, duration, and the sample tables that map timestamps to byte offsets. FFmpeg can't decode a single frame without it, which is why a missing moov is a hard failure rather than a warning.
FFmpeg writes moov at the end of the file by default, because the sample tables aren't final until the last frame is muxed. That default is the root cause of both failure modes. If the writer dies before it finishes, everything you have is ftyp plus a partial mdat with no index. And if the file is complete but your reader only pulled the first few megabytes, you got a valid mdat prefix and no index either. The bytes are innocent in the second case. The read is what's broken.
The fluent-ffmpeg tracker has a long-running thread on it (issue #1022) from Node.js users processing uploads, and AWS re:Post documents MediaConvert reporting "no moov box found in file" when the source .mov has its moov at the end and Range requests aren't handled the way the muxer expects.
Three checks that tell you which failure you have
Run these in order before you touch the file.
- Compare byte counts. Ask the origin what it thinks the file is, then check what you got.
curl -sI "$URL" | tr -d '\r' | grep -i '^content-length'
curl -s -o /tmp/in.mp4 "$URL"
stat -c%s /tmp/in.mp4 # Linux; use stat -f%z on macOS
If those two numbers differ, stop. You have a truncated file or a partial read, and no FFmpeg flag fixes that. Also check the status code: a 206 where you expected 200 means something in your stack (an SDK default, a CDN, a resumable-download helper) sent a Range header you didn't write.
curl -s -o /dev/null -w '%{http_code} %{size_download}\n' "$URL"
- Walk the top-level boxes. This tells you whether a
moovexists at all and where it sits, without decoding anything.
import sys, struct
with open(sys.argv[1], 'rb') as f:
off = 0
while True:
hdr = f.read(8)
if len(hdr) < 8:
break
size, typ = struct.unpack('>I4s', hdr)
if size == 1:
size = struct.unpack('>Q', f.read(8))[0]
print(f"{typ.decode('latin1')} @ {off} ({size} bytes)")
if size == 0:
break
off += size
f.seek(off)
A healthy web-ready file prints ftyp, then moov, then mdat. A file that needs remuxing prints ftyp, mdat, moov. A truncated file prints ftyp, mdat, and then nothing, because the script runs off the end of the file.
- Confirm with ffprobe. If ffprobe reads the local copy cleanly, the file is fine and your fetch was the problem.
ffprobe -v error -show_entries format=duration,size,format_name \
-of default=noprint_wrappers=1 /tmp/in.mp4
Fixing a complete file whose index is at the end
When the byte counts match and the box walk shows moov after mdat, remux the file. Remuxing copies the streams without re-encoding and moves the index to the front:
ffmpeg -i input.mp4 -c copy -movflags +faststart output.mp4
Because -c copy skips the encoder entirely, a 200 MB 1080p MP4 remuxes in a couple of seconds on ordinary disk. The second pass only shifts the moov forward and fixes up the offsets, so it's I/O bound, not CPU bound. Nothing about the video changes: same codecs, same bitrate, same duration.
The other half of the fix is on the read side. If your worker or your downstream service issues Range requests, make sure a request for the beginning of an unknown file can still reach the index, or fetch the object whole before you hand it to a demuxer. Partial reads against an unmodified export from an encoder that wrote moov last will fail every time, however healthy the object in S3 is.
Preventing it at ingest instead of debugging it later
Every MP4 your own pipeline generates should be written with -movflags +faststart, and every file entering your pipeline should be validated before it gets queued. Those two habits remove most of this error class permanently.
For files you produce:
ffmpeg -i input.mov -c:v libx264 -crf 21 -c:a aac -b:a 128k \
-movflags +faststart output.mp4
If you're writing to a pipe, a socket, or anything else non-seekable, faststart can't work, because there's nothing to rewind and rewrite. Use a fragmented MP4 instead, which puts an empty moov at the front immediately:
ffmpeg -i input.mov -c copy -movflags frag_keyframe+empty_moov -f mp4 pipe:1
For files you receive, validate on arrival: check the uploaded size against the declared Content-Length, run ffprobe, and reject with a message the uploader can act on ("upload incomplete, 41 MB of 118 MB received") instead of letting a truncated object sit in a bucket until a render job trips over it three hours later. On S3, also add a lifecycle rule with AbortIncompleteMultipartUpload, and audit what's already stuck:
aws s3api list-multipart-uploads --bucket my-ingest-bucket
Incomplete multipart uploads are invisible in the console's object list but still hold the partial bytes, which is exactly how a "file that was definitely uploaded" turns out to be half a file.
That validate-then-remux step is a small service to own: an encoder to install, a version to pin, a box to keep warm, timeouts to handle when a 4 GB source shows up. FFmpeg Micro does the same thing as one job submission, so the probe, the faststart remux, and the download all happen server-side and your worker just reads the result. The docs show the job semantics: submit, poll or take a webhook, fetch the output. Skip the Media Service walks through that trade, and FFmpeg in Docker covers what the self-hosted version costs to maintain.
Running FFmpeg yourself vs sending the file to an API
Both paths use the same FFmpeg flags. The difference is who runs the encoder and who handles the failure cases around it.
| Step | Your own worker | One API call |
|---|---|---|
| Probe the input | Install FFmpeg, pin the version, keep it in the image | Included in the job |
| Faststart remux | `ffmpeg -c copy -movflags +faststart`, needs seekable local disk | Same operation, server-side |
| Large-file downloads | Your timeout, your retries, your disk | Pass the source URL |
| Long jobs | Worker lifetime and Lambda's 15-minute ceiling | Poll or webhook |
| Calling it from n8n, Make, or Zapier | Needs an HTTP service you host | Native HTTP node |
Common pitfalls
- Piping faststart output.
-movflags +faststarttopipe:1fails with a non-seekable-output error. Write to a real file, or usefrag_keyframe+empty_moov. - Testing on a cached copy. Your laptop plays the file fine because your browser downloaded all of it. Reproduce the failure with the same fetch your worker uses, not a manual download.
- Assuming
Content-Lengthis always there. Servers using chunked transfer encoding don't send one, so byte-count validation needs a fallback: probe the file and check that the reported duration is non-zero. - Re-encoding when you only needed to remux. Dropping
-c copyturns a 2-second container rewrite into a multi-minute transcode and quietly costs you a generation of quality. - Treating a truncated upload as a decode bug. If the bytes never arrived, retrying FFmpeg with different flags cannot help. Re-fetch or reject.
When the file really is truncated
If the byte counts don't match and the original source is gone, you're doing repair, not decoding, and the odds are mediocre. The open-source tool untrunc can sometimes rebuild a missing moov by borrowing the structure of a healthy reference file recorded by the same device and settings, which is a real constraint: no matching reference, no repair. Expect to lose the tail of the recording either way, since those frames were never written.
That's the honest boundary here. A video API, including ours, processes bytes that exist. Restoring bytes that were never written to disk is the job of a consumer data-recovery tool, and a physically failing card is that software's problem, not a pipeline's. The better investment is making truncation impossible to ingest silently.
FAQ
What does "moov atom not found" mean in FFmpeg?
FFmpeg prints "moov atom not found" when it reaches the end of the data it was given without finding the moov box that indexes an MP4 or MOV file. The two common causes in a pipeline are a truncated upload where the writer died before the trailing moov was flushed, and a complete file whose moov sits at the end but whose bytes were only partially fetched by a Range request.
Can I fix a truncated MP4 that has no moov atom?
A truncated MP4 can sometimes be partially recovered. Tools like untrunc rebuild an index by comparing the broken file to a healthy reference clip from the same camera or encoder, and the frames that were never written are gone regardless. Re-uploading the original file is faster and more reliable whenever it still exists.
What does -movflags +faststart actually do?
-movflags +faststart tells the MP4 muxer to run a second pass after encoding that moves the moov atom from the end of the file to the front and rewrites the internal byte offsets to match. The output is byte-for-byte identical in video and audio content, and it starts playing in a browser before the whole file has downloaded.
Why does the file work on my machine but fail in the pipeline?
A local player downloads or reads the entire file, so it always finds the moov at the end. A pipeline worker often reads a range, a stream, or a partial object, so it hits the mdat and stops before reaching the index. Reproduce with the exact fetch your worker performs, then compare downloaded bytes to Content-Length.
Should every video I generate use faststart?
Every video headed for HTTP playback or another service should use faststart. The cost is one extra pass over the container at write time, and it removes an entire failure mode for every downstream consumer. The exception is non-seekable output, where fragmented MP4 with frag_keyframe+empty_moov gives you the same front-loaded index, and HLS output, which uses segments instead (creating an HLS stream covers that path).
Wire the probe-and-remux step into your ingest path once and this error stops reaching your on-call rotation. If you'd rather not run the encoder that does it, sign up free and send the file as a single job, from your code, from n8n, Make, or Zapier, or from an AI agent, and get a faststart MP4 back.
About Javid Jamae
Founder & CEO at FFmpeg Micro
Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.
You might also like

ffmpeg' Is Not Recognized: It's Your PATH, Not the Install
Fix 'ffmpeg' is not recognized as an internal or external command. Diagnose ffmpeg not recognized: install check, PATH bin folder, stray spaces, restart.

Skip the Media Service: Video Processing API for a SaaS Product
A video processing API for a SaaS product beats a media microservice: real TCO of FFmpeg on Lambda vs Fargate, plus a one-call endpoint you ship today.

Put Two Videos Side by Side with FFmpeg: hstack, vstack, xstack
The ffmpeg side by side recipe that actually works: hstack needs matching heights, vstack matching widths, and shortest=1 stops the short clip freezing.
Ready to process videos at scale?
Start using FFmpeg Micro's simple API today. No infrastructure required.
Get Started Free