We use Remotion to render video clips server-side, and we pre-build a Vercel Sandbox snapshot at deploy time so renders can restore it for fast cold starts instead of bundling from scratch on every request.
The flow looks like this:
-
next buildcompiles the Next.js app -
node scripts/create-snapshot.mjsbundles the Remotion project, uploads it to a sandbox, takes a snapshot, and stores the snapshot ID in Vercel Blob - Render workers restore the snapshot instead of re-bundling
Last week, two separate bugs caused this to silently break in production. Here's what happened and what we changed.
Bug 1: Relative path passed to addBundleToSandbox
The @remotion/vercel package's addBundleToSandbox function uploads every file in the bundle directory to the sandbox. Under the hood it reads the directory, creates each file path in the sandbox, and streams the content.
The bug: we were passing a relative path:
const BUNDLE_DIR = ".remotion";
// ...
await addBundleToSandbox({ sandbox, bundleDir: BUNDLE_DIR });
This works fine when Node.js's CWD happens to be the project root — which it is locally and in most CI setups. But in certain Vercel build environments, the CWD at script execution time drifts from what you'd expect, and the relative path resolves to the wrong directory. The upload silently succeeds with zero or wrong files, and the snapshot is empty.
The fix is one line — resolve to an absolute path from the script's own __dirname:
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const BUNDLE_DIR = ".remotion";
// Before (fragile):
await addBundleToSandbox({ sandbox, bundleDir: BUNDLE_DIR });
// After (always correct):
const bundleDir = path.resolve(__dirname, `../${BUNDLE_DIR}`);
await addBundleToSandbox({ sandbox, bundleDir });
Lesson: never trust relative paths in build scripts. Scripts can be invoked from anywhere. Always anchor to __dirname (or import.meta.url in ESM).
Bug 2: addBundleToSandbox chokes on subdirectories
Once we fixed the path, we hit a second failure: addBundleToSandbox was erroring when the bundle output contained a public/ subdirectory.
Remotion's bundler copies your project's public/ folder into the output by default. The sandbox API's internal mkDir call doesn't create parent directories recursively — it expects a flat structure. So any nested path like public/fonts/Inter.woff2 triggers an error because public/ wasn't created first.
There are two ways to fix this:
Option A: Pass publicDir: null to bundle() to skip copying the public folder entirely (works if your compositions don't depend on static assets from public/):
await bundle({
entryPoint,
outDir: bundleDir,
enableCaching: true,
publicDir: null, // don't copy public/ into the bundle
});
Option B: Let bundle() copy it, then delete it before uploading:
import { rm } from "fs/promises";
await bundle({ entryPoint, outDir: bundleDir, enableCaching: true });
// Remove public/ before upload — addBundleToSandbox can't handle subdirs
const publicDir = path.join(bundleDir, "public");
await rm(publicDir, { recursive: true, force: true });
await addBundleToSandbox({ sandbox, bundleDir });
We went with both: publicDir: null in bundle() plus the defensive rm as a belt-and-suspenders guard:
await bundle({
entryPoint,
outDir: bundleDir,
enableCaching: true,
publicDir: null,
});
// Defensive cleanup in case future Remotion versions change the default
const publicDir = path.join(bundleDir, "public");
await rm(publicDir, { recursive: true, force: true });
Bug 3 (Bonus): The snapshot failure was non-fatal
Both bugs above caused the snapshot script to error — but we had the error suppressed:
// package.json (before)
"vercel-build": "next build && node scripts/create-snapshot.mjs || echo '[create-snapshot] Skipped (non-fatal)'"
The || echo fallback was added during initial development so a missing BLOB_READ_WRITE_TOKEN wouldn't break staging deploys. But it meant snapshot failures in production were completely silent — the build succeeded, workers tried to restore a snapshot that didn't exist, and renders fell back to a full re-bundle (slow, and occasionally OOM).
The fix: remove the fallback. Let it fail the build loud:
// package.json (after)
"vercel-build": "next build && node scripts/create-snapshot.mjs"
If the snapshot step fails, the deployment fails. That's the right behavior — you find out immediately instead of running degraded for days.
Takeaways
- Always use absolute paths in build scripts. CWD is an environment variable, not a constant.
- Know your tool's flat-file assumptions. The Remotion Vercel sandbox API expects a flat bundle — no subdirectories. The bundler doesn't tell you this.
-
Don't swallow errors in build steps. The
|| echopattern is fine for truly optional steps. If a step is required for production correctness, let it fail the build.
All three changes shipped in one small commit. The snapshot build is now fast, reliable, and loud when something goes wrong.
Top comments (0)