DEV Community

Javid Jamae
Javid Jamae

Posted on • Originally published at ffmpeg-micro.com

Running FFmpeg on Heroku: The Ephemeral-Filesystem Trap (and the Fix)

Originally published at ffmpeg-micro.com

Your FFmpeg pipeline works on localhost. You push to Heroku, trigger a transcode, and the job vanishes. No output file. No error you can Google. Just a request that hangs and then dies.

This isn't a config mistake. Heroku's architecture is fundamentally hostile to long-running FFmpeg jobs, and the buildpack README won't tell you that.

The three ways Heroku kills your FFmpeg jobs

Most developers hit one of these. Some hit all three in the same week.

The ephemeral filesystem eats your output

Heroku dynos don't have persistent storage. Every file you write goes to /tmp, and /tmp gets wiped on every dyno restart. Deploys, config changes, Heroku's own dyno cycling (which happens at least once every 24 hours). All of it triggers a restart.

If your FFmpeg job takes 4 minutes to transcode a video and Heroku cycles the dyno at minute 3, the output file is gone. No partial recovery. No resume. The work just disappears.

This is the one that burns people the hardest because it works fine during testing. Short test files finish before the cycle hits. Production files with real-world durations don't.

The buildpack bloats your slug

The standard heroku-buildpack-ffmpeg-latest adds roughly 80MB to your slug size. Heroku caps slugs at 500MB. That sounds like plenty until you account for your Node modules, your Python dependencies, your static assets, and whatever else your app ships.

A mid-sized Rails or Next.js app can easily sit at 350-400MB before adding FFmpeg. Drop in the buildpack and you're flirting with the limit. Add a second binary or a few large dependencies and the deploy fails outright.

You can strip FFmpeg down to only the codecs you need, but that means maintaining a custom buildpack fork. Now you're debugging FFmpeg compilation flags on top of everything else.

The 30-second timeout kills long encodes

Heroku web dynos enforce a hard 30-second request timeout at the router level. This isn't configurable. If your HTTP request doesn't return a response within 30 seconds, Heroku's router drops the connection and returns an H12 error.

FFmpeg transcoding a 5-minute video at reasonable quality takes longer than 30 seconds. Every time.

The standard workaround is moving FFmpeg to a worker dyno with a job queue. That means adding Redis, adding a queue library (Sidekiq for Ruby, Bull for Node), writing the queue/worker plumbing, and handling job status polling from your frontend. Your "just run FFmpeg" feature now requires three additional infrastructure components and a new failure surface.

Worker dynos don't have the 30-second timeout, but they still have the ephemeral filesystem problem. You've solved one issue and kept another.

The honest fork: harden it or offload it

You have two real options. Neither is wrong, but they lead to very different maintenance burdens.

Option A: Harden the Heroku setup

This means:

  • Streaming FFmpeg output directly to S3 instead of writing to /tmp (using pipes or chunked uploads)
  • Running FFmpeg on worker dynos with Redis and a job queue
  • Pinning your buildpack to a specific FFmpeg version so deploys don't break when upstream changes
  • Building health checks and retry logic for jobs that die mid-transcode

This works. Teams do run FFmpeg on Heroku in production. But you're maintaining a distributed video processing pipeline on top of an app platform that wasn't designed for it. Every piece (S3 streaming, queue infrastructure, retry logic, buildpack maintenance) is another thing that can break at 2am.

Option B: Keep the dyno stateless

Instead of fighting Heroku's architecture, work with it. Keep your dyno doing what dynos are good at (serving requests, running application logic) and send the FFmpeg work to an API that handles the filesystem, the timeouts, and the binary management for you.

Your Heroku app sends a POST request with the input file URL and the FFmpeg options. The transcode runs on infrastructure built for it. Your dyno stays stateless, your slug stays small, and you don't need Redis.

A single API call from your Heroku app replaces the entire worker dyno + queue + S3 pipeline:

curl -X POST https://api.ffmpeg-micro.com/v1/transcodes \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "inputs": [{"url": "https://your-s3-bucket.amazonaws.com/input.mp4"}],
    "outputFormat": "mp4",
    "options": [
      {"option": "-c:v", "argument": "libx264"},
      {"option": "-crf", "argument": "23"}
    ]
  }'
Enter fullscreen mode Exit fullscreen mode

The input file gets pulled from your S3 bucket (or any public URL). The transcode runs on FFmpeg Micro's infrastructure. You poll for completion or set up a webhook. No buildpack, no worker dyno, no ephemeral filesystem.

Your Heroku app stays a Heroku app. The video processing happens somewhere that won't wipe your files mid-job.

The real cost isn't the first deploy

Getting FFmpeg running on Heroku isn't the hard part. Keeping it running is. The dyno cycling, the slug creep as your app grows, the queue jobs that silently fail, the buildpack that pulls a new FFmpeg version and breaks your encoding flags.

Every hour you spend debugging Heroku-specific FFmpeg failures is an hour you're not building the feature your users actually asked for.

FAQ

Why does FFmpeg work locally but fail on Heroku?

Local machines have persistent filesystems and no request timeouts. Heroku dynos have ephemeral storage that wipes on restart and a 30-second router timeout on web dynos. FFmpeg jobs that finish fine on your laptop will fail in production if they write to disk or take longer than 30 seconds.

Can I increase the Heroku dyno timeout for video processing?

No. The 30-second timeout on web dynos is enforced at the Heroku router and is not configurable. Your options are moving FFmpeg to a background worker dyno with a job queue (Redis + Sidekiq or Bull) or offloading the transcode to an external API.

What is the best FFmpeg buildpack for Heroku?

The most common is heroku-buildpack-ffmpeg-latest, which pulls the current FFmpeg build. It adds roughly 80MB to your slug. For production use, pin it to a specific FFmpeg version so your deploys don't break when upstream releases a new build with different codec defaults.

How do I handle large video files on Heroku's ephemeral filesystem?

Don't write to the local filesystem for anything you need to keep. Stream FFmpeg output directly to S3 using pipes, or use a hosted FFmpeg API that handles storage independently. Any file written to /tmp on a Heroku dyno will be lost on the next restart, which happens at least once every 24 hours.

Is Heroku a good platform for video processing?

Heroku is built for stateless web applications, not compute-heavy media processing. You can make it work with worker dynos, job queues, and external storage, but you're adding significant infrastructure complexity. If video processing is a core feature of your app, purpose-built infrastructure (or a hosted API) will save you maintenance time.

Top comments (0)