DEV Community

nareshipme
nareshipme

Posted on

Two Subtle Bugs That Broke Our Remotion Vercel Sandbox (And How We Fixed Them)

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:

  1. next build compiles the Next.js app
  2. node scripts/create-snapshot.mjs bundles the Remotion project, uploads it to a sandbox, takes a snapshot, and stores the snapshot ID in Vercel Blob
  3. 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 });
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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)'"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

If the snapshot step fails, the deployment fails. That's the right behavior — you find out immediately instead of running degraded for days.


Takeaways

  1. Always use absolute paths in build scripts. CWD is an environment variable, not a constant.
  2. 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.
  3. Don't swallow errors in build steps. The || echo pattern 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)