Skip to content
Guides · Recipes

How to Build an FFmpeg Video Pipeline in a Cloud Sandbox

Updated October 27, 2026 .md

Short answer: FFmpeg 6.1.1 with x264, x265, SVT-AV1, VP9 and Opus is preinstalled in every sandbox, so a pipeline is one script. On 60 seconds of 1080p video, a 720p H.264 encode took 18 s on small and 13 s on default. A full 10.5-minute film took 188 s and 113 s.

What we ran#

Everything below ran on prod on October 27, 2026, in two sandboxes: one small (2 vCPU, 4 GB, $0.018/hour) and one default (4 vCPU, 8 GB, $0.036/hour). The source is Big Buck Bunny (Blender Foundation, CC BY 3.0): 1080p30 H.264, 634.6 seconds. Each timing is a single run, so treat the numbers as a guide, not a promise.

Step small default
Download the 263 MB source inside the sandbox 3.1 s not timed
Full pipeline on a 60 s clip (720p MP4, 2-rung HLS, thumbnails, poster) 47.8 s 26.2 s
720p H.264 veryfast, 60 s clip 18.0 s (3.3x realtime) 13.2 s (4.6x)
720p H.264 veryfast, whole 634.6 s film 188.3 s (3.4x) 113.2 s (5.6x)
1080p AV1 (SVT-AV1 preset 8), 60 s clip 133.3 s (0.45x) 85.7 s (0.70x)
Hosted HLS output on a private URL 401 without the token, 200 with it
sbx stop --wait, then sbx resume memoryRestored: true, the file server kept serving

1. Check what FFmpeg can encode#

You don't install anything. FFmpeg comes with the image (what's preinstalled), built with the encoders most pipelines need:

id=$(sbx new --type small --json | jq -r 'select(.event == "ready") | .id')
sbx exec $id 'ffmpeg -version | head -1; ffmpeg -hide_banner -encoders 2>/dev/null | grep -E "libx264|libx265|libsvtav1|libvpx-vp9|libopus|libmp3lame"'
ffmpeg version 6.1.1-3ubuntu5 Copyright (c) 2000-2023 the FFmpeg developers
 V..... libsvtav1            SVT-AV1(Scalable Video Technology for AV1) encoder (codec av1)
 V....D libx264              libx264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 (codec h264)
 V....D libx264rgb           libx264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 RGB (codec h264)
 V....D libx265              libx265 H.265 / HEVC (codec hevc)
 V....D libvpx-vp9           libvpx VP9 (codec vp9)
 A....D libmp3lame           libmp3lame MP3 (MPEG audio layer 3) (codec mp3)
 A....D libopus              libopus Opus (codec opus)

There is no GPU, so there is no NVENC or VAAPI. Every encode on this page is on the CPU.

2. Pull the source into the sandbox#

Fetch inputs from inside the sandbox rather than uploading them from your laptop. The sandbox downloaded the 263 MB file in 3.1 seconds:

sbx exec $id --timeout 600 'cd ~ && curl -sSfL -o bbb.zip https://download.blender.org/demo/movies/BBB/bbb_sunflower_1080p_30fps_normal.mp4.zip \
  && unzip -q bbb.zip && rm bbb.zip && mv bbb_sunflower_1080p_30fps_normal.mp4 bbb-full.mp4 \
  && ffmpeg -hide_banner -loglevel error -y -i bbb-full.mp4 -t 60 -map 0:v -map 0:a:0 -c copy clip60.mp4 \
  && ffprobe -v error -show_entries format=duration,bit_rate:stream=codec_type,codec_name,width,height -of compact bbb-full.mp4'
stream|codec_name=h264|codec_type=video|width=1920|height=1080
stream|codec_name=mp3|codec_type=audio
stream|codec_name=ac3|codec_type=audio
format|duration=634.600000|bit_rate=3481058

The file has two audio tracks, so the commands below map the first one (0:a:0) on purpose. Otherwise FFmpeg picks one for you. sbx exec stops synchronous commands after 30 seconds unless you pass --timeout (600 at most). Use --detach for anything longer (step 5).

3. The pipeline script#

One script turns an upload into what a video page needs: a probe, a 720p MP4 for download, an HLS ladder (720p and 360p) for streaming, a thumbnail sheet and a poster frame. It prints how long each step took.

#!/usr/bin/env bash
# pipeline.sh <input> <outdir>: probe, 720p MP4, 2-rung HLS ladder, poster and thumbnail sheet
set -euo pipefail
in=$1; out=$2; mkdir -p "$out/hls"
t() { local s=$(date +%s.%N); "$@" ; awk -v a="$s" -v b="$(date +%s.%N)" -v n="$step" 'BEGIN{printf "%-7s %6.1f s\n", n, b-a}' >&2; }
q="-hide_banner -loglevel error -y"

step=probe  t ffprobe -v error -show_entries format=duration:stream=codec_name,width,height,r_frame_rate -of json "$in" > "$out/probe.json"
step=mp4    t ffmpeg $q -i "$in" -vf scale=-2:720 -c:v libx264 -preset veryfast -crf 23 \
                -c:a aac -b:a 128k -movflags +faststart "$out/720p.mp4"
step=hls    t ffmpeg $q -i "$in" -filter_complex "[0:v]split=2[a][b];[a]scale=-2:720[v720];[b]scale=-2:360[v360]" \
                -map "[v720]" -map "[v360]" -map 0:a -map 0:a \
                -c:v libx264 -preset veryfast -g 60 -keyint_min 60 -sc_threshold 0 \
                -b:v:0 3000k -maxrate:v:0 3300k -bufsize:v:0 6000k \
                -b:v:1 800k -maxrate:v:1 880k -bufsize:v:1 1600k -c:a aac -b:a 128k \
                -f hls -hls_time 4 -hls_playlist_type vod -master_pl_name master.m3u8 \
                -var_stream_map "v:0,a:0 v:1,a:1" -hls_segment_filename "$out/hls/%v_%03d.ts" "$out/hls/%v.m3u8"
step=thumbs t ffmpeg $q -i "$in" -vf "fps=1/2,scale=320:-2,tile=5x2" -frames:v 1 "$out/thumbs.jpg"
step=poster t ffmpeg $q -ss 5 -i "$in" -frames:v 1 "$out/poster.jpg"

The HLS step decodes the source once and splits it into both renditions. -g 60 -sc_threshold 0 puts a keyframe every 2 seconds, so every 4-second segment starts on one. Each flag is in the FFmpeg formats docs.

sbx scp ./pipeline.sh $id:/home/user/
sbx exec $id --timeout 600 'chmod +x ~/pipeline.sh && cd ~ && ./pipeline.sh clip60.mp4 out && cat out/hls/master.m3u8'

On small:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=3440800,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2"
0.m3u8

#EXT-X-STREAM-INF:BANDWIDTH=1020800,RESOLUTION=640x360,CODECS="avc1.64001e,mp4a.40.2"
1.m3u8

probe      0.1 s
mp4       20.0 s
hls       25.3 s
thumbs     2.0 s
poster     0.4 s

On default, the same script took 0.0, 9.7, 14.9, 1.2 and 0.4 seconds. (The small run above predates the bitrate cap described next; the default run includes it.)

Cap the bitrate. Our first small run had only -b:v:0 3000k. One 4-second 720p segment came out at 4.4 MB, about 8.9 Mbit/s, roughly three times the bitrate the playlist advertises. That can make players pick a rendition the viewer's connection can't sustain. With -maxrate and -bufsize added (as in the script above), the same segment was 2.0 MB.

4. Encode speed per size#

We ran the same benchmark script on both sizes. "Realtime" is seconds of video divided by seconds of encoding. Output sizes matched within 0.5 MB across sizes: the CPU count changes speed, not the result.

Encode (60 s of 1080p30 unless noted) small default Output
720p H.264, -preset veryfast -crf 23 18.0 s, 3.33x 13.2 s, 4.56x 9.6 MB
720p H.264, -preset medium -crf 23 37.8 s, 1.59x 21.2 s, 2.83x 13.3 MB
1080p H.264, -preset veryfast -crf 23 36.7 s, 1.64x 19.5 s, 3.08x 18.0 MB
1080p H.265, -preset fast -crf 28 86.9 s, 0.69x 42.1 s, 1.42x 10.3 MB
1080p AV1, SVT-AV1 -preset 8 -crf 35 133.3 s, 0.45x 85.7 s, 0.70x 13.1 MB
Whole film (634.6 s) to 720p H.264 veryfast 188.3 s, 3.37x 113.2 s, 5.61x 110 MB

What it costs: at those whole-film speeds, one hour of source video to 720p H.264 takes about 18 minutes on small (about $0.005) and about 11 minutes on default (about $0.006). small is slightly cheaper per minute of video; default finishes sooner. Pick default when someone is waiting for the result, and small for an overnight backlog. Prices come from the pricing page.

The default sandbox ran on an AMD Ryzen 9 7950X3D host. We didn't record the small sandbox's host CPU, so part of the gap may come from the host, not only the vCPU count.

5. Run several encodes at once, or detach long ones#

x264 doesn't use 4 vCPUs fully on a single 720p encode. On default, four 720p veryfast encodes of the 60-second clip took:

How Wall time
One after another (4 x 13.2 s) 52.8 s
All four at once with & and wait 39.1 s
All four at once, each with -threads 1 34.8 s

For a batch, run one FFmpeg per vCPU with -threads 1 instead of one big multi-threaded job.

The whole-film encodes ran longer than sbx exec waits, so we started the benchmark detached and polled it:

pid=$(sbx exec $id --detach 'bash ~/bench.sh' | jq -r .processId)
while [ "$(sbx exec $id --status $pid --json | jq -r .running)" = true ]; do sleep 30; done
sbx exec $id --status $pid --json | jq -r '.exitCode, .stdout'
0
720p x264 veryfast            13.2 s   4.56x realtime  rc=0
…
full film 720p veryfast      113.2 s   5.61x realtime  rc=0

A running detached job counts as activity for idle pause, but it doesn't hold off auto-stop. For jobs that take hours, see run a long job and check on it later.

6. Serve the HLS output on a private URL#

To play the result in a browser or check it with a real player, serve the output folder and host the port. A systemd unit restarts the server if it crashes, and again on any boot:

# hls-web.service
[Unit]
Description=Serve ~/out over HTTP for HLS playback
After=network.target

[Service]
User=user
WorkingDirectory=/home/user/out
ExecStart=/usr/bin/python3 -m http.server 8080 --bind 0.0.0.0
Restart=always

[Install]
WantedBy=multi-user.target
sbx scp ./hls-web.service $id:/tmp/
sbx exec $id 'sudo mv /tmp/hls-web.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl enable --now hls-web && systemctl is-active hls-web'
URL=$(sbx host $id 8080 --title "HLS output" --json | jq -r .url)   # private by default

The URL is https://<subdomain>-8080.box.sandboxer.com?_token=…. Keep it out of logs. We checked it from a laptop:

BASE=${URL%%\?*}
curl -s -o /dev/null -w '%{http_code}\n' "$BASE/hls/master.m3u8"          # no token
curl -s -c jar -b jar -L -o /dev/null -w '%{http_code} after %{num_redirects} redirect\n' \
  "$BASE/hls/master.m3u8?_token=${URL##*_token=}"                          # token sets a cookie
curl -s -b jar -o /dev/null -w '%{http_code} %{size_download} bytes %{time_total}s\n' "$BASE/hls/0_001.ts"
401
200 after 1 redirect
200 4430972 bytes 2.072551s

The first request with _token returns a 302 that sets a cookie. After that, plain paths work, which is what a player needs when it fetches 0.m3u8 and the segments relative to the master playlist. That 4.4 MB segment (from the uncapped first run) took about 2 seconds to download from our laptop, so playback keeps ahead. Your network sets that number. For a public demo, sbx host $id 8080 --public drops the token. See give a sandbox port an HTTPS URL for the details.

7. What survives a stop#

sbx stop $id --wait
sbx resume $id
sbx info $id --json | jq '.sandbox | {memoryRestored, stop: .stop.reason}'
{
  "memoryRestored": true,
  "stop": "user"
}

The output files, the file server and the hosted URL all came back: the same segment URL returned 200 with the same 4,430,972 bytes. If a resume lands on a machine with a different CPU type, the sandbox boots from disk (memoryRestored: false). Your files stay, but running processes are lost. The enabled hls-web unit starts again on that boot. A half-finished FFmpeg job doesn't, so rerun it. We didn't trigger a cold boot in this test.

8. Get the results out#

sbx scp $id:/home/user/out/720p.mp4 .

The 10.1 MB file took 5.3 seconds to copy to our laptop, tunnel setup included. For whole folders, rsync and the SDK limits, see move files in and out of a sandbox. Delete the sandbox when you're done: sbx delete $id --yes.

FAQ#

Is FFmpeg already installed in a Sandboxer sandbox?#

Yes. Version 6.1.1 from Ubuntu 24.04, with libx264, libx265, SVT-AV1, libvpx-vp9, Opus and MP3.

Which sandbox size should I use for transcoding?#

default encoded 1.4x to 2.1x faster than small in our runs, and cost a little more per minute of video. Use default when a user is waiting, and small for batch backlogs. Encoding to AV1 or H.265 at 1080p ran below realtime on small.

Can I use GPU encoding such as NVENC?#

No. Sandboxes have no GPU, so use the CPU encoders. For H.264 at 720p, veryfast ran at 3.3x to 5.6x realtime in our tests.

How do I process files longer than the exec timeout?#

Start FFmpeg with sbx exec --detach, which returns a process id at once, then check sbx exec --status. Synchronous exec stops at 600 seconds even with --timeout.

Can a browser play the HLS output straight from the sandbox?#

Yes. Host the port that serves the folder. With a private URL, the first request needs ?_token=. It sets a cookie, so the player's follow-up requests for playlists and segments work without the token.

More guides

Top AI sandbox platforms for code execution in 2026, ranked

For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.

Read the guide
Best sandboxes for running coding agents (Claude Code, Codex, Gemini CLI) in 2026, ranked

For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.

Read the guide
Best Daytona sandbox alternatives in 2026, ranked

The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.

Read the guide
Best E2B alternatives in 2026, ranked

If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.

Read the guide