How to Use FFmpeg with Trigger.dev (No Binary Setup Required)

Trigger.dev's official FFmpeg guide tells you to install the @trigger.dev/build extension, bundle a static FFmpeg binary into your task, and call it with ffmpegPath(). It works. Until your build artifact balloons past 100 MB, your cold starts crawl, and you're debugging codec availability across deployment environments.
You don't need to ship FFmpeg with your Trigger.dev task. Call an FFmpeg API over HTTP instead, and keep your tasks lean.
> Quick answer: Replace the @trigger.dev/build FFmpeg extension with HTTP calls to FFmpeg Micro. POST your video URL to /v1/transcodes, poll for completion, and grab the download link. No binary in your build, no cold-start penalty, and it works on any Trigger.dev deployment target.
What breaks when you bundle FFmpeg as a binary
The @trigger.dev/build approach uses ffmpegExtension() to include a static FFmpeg binary in your task's build output. Trigger.dev's own docs walk through this pattern. For simple cases, it's fine. But production workloads expose several problems:
- Build size bloat. A static FFmpeg binary adds 80-120 MB to your task artifact. Every deploy ships that weight, and every cold start loads it into memory.
- Cold-start latency. Larger artifacts mean slower cold starts. If your task scales to zero between runs, each invocation pays the binary loading tax.
- Codec availability. Static builds may lack specific codecs you need. Want
libx265orlibvpx-vp9? You're recompiling or hoping the bundled binary includes it. - Version management. Pinning FFmpeg versions across environments is your problem. Updates mean rebuilding and redeploying.
None of these issues exist when FFmpeg runs on someone else's infrastructure.
The binary approach (before)
This is what Trigger.dev's guide recommends. You install the extension, configure your build, and call FFmpeg from the task:
// trigger.config.ts
import { ffmpegExtension } from "@trigger.dev/build/extensions";
export default defineConfig({
build: {
extensions: [ffmpegExtension()],
},
});
// tasks/process-video.ts
import { task } from "@trigger.dev/sdk/v3";
import { ffmpegPath } from "@trigger.dev/build/extensions";
import { execSync } from "child_process";
export const processVideoTask = task({
id: "process-video",
run: async (payload: { videoUrl: string }) => {
const inputPath = "/tmp/input.mp4";
const outputPath = "/tmp/output.mp4";
// Download the file first
execSync(`curl -o ${inputPath} "${payload.videoUrl}"`);
// Run FFmpeg binary
execSync(`${ffmpegPath()} -i ${inputPath} -vf scale=1280:720 -crf 23 ${outputPath}`);
// Upload output somewhere...
},
});
You're managing file I/O, disk space in /tmp, the binary path, and cleanup. The task does too much.
Drop the binary: FFmpeg over HTTP in Trigger.dev
Replace all of that with three HTTP calls. No extension, no binary, no file system management.
import { task } from "@trigger.dev/sdk/v3";
export const processVideoTask = task({
id: "process-video",
run: async (payload: { videoUrl: string }) => {
const API_KEY = process.env.FFMPEG_MICRO_API_KEY;
const BASE_URL = "https://api.ffmpeg-micro.com";
// 1. Start the transcode job
const createRes = await fetch(`${BASE_URL}/v1/transcodes`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
inputs: [{ url: payload.videoUrl }],
outputFormat: "mp4",
preset: { quality: "high" },
}),
});
const job = await createRes.json();
// 2. Poll until complete
let status = job.status;
while (status !== "completed" && status !== "failed") {
await new Promise((r) => setTimeout(r, 5000));
const pollRes = await fetch(`${BASE_URL}/v1/transcodes/${job.id}`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
const pollData = await pollRes.json();
status = pollData.status;
}
if (status === "failed") {
throw new Error(`Transcode failed for job ${job.id}`);
}
// 3. Get the download URL
const downloadRes = await fetch(
`${BASE_URL}/v1/transcodes/${job.id}/download`,
{ headers: { Authorization: `Bearer ${API_KEY}` } }
);
const { url } = await downloadRes.json();
return { downloadUrl: url };
},
});
That's the entire task. No trigger.config.ts changes. No build extensions. No /tmp disk management. Your task artifact stays small, cold starts stay fast, and FFmpeg version management is someone else's problem.
Using custom FFmpeg options
The preset field handles common operations, but you can pass raw FFmpeg flags through the options array when you need fine-grained control. Resize to 720p, set a specific CRF, or choose a codec:
body: JSON.stringify({
inputs: [{ url: payload.videoUrl }],
outputFormat: "mp4",
options: [
{ option: "-vf", argument: "scale=1280:720" },
{ option: "-c:v", argument: "libx264" },
{ option: "-crf", argument: "23" },
{ option: "-c:a", argument: "aac" },
{ option: "-b:a", argument: "128k" },
],
}),
The outputFormat accepts mp4, webm, or mov. Use -vf for video filters. Note that -filter_complex is not supported. Use -vf for all filter operations.
Handling file uploads in your task
If your Trigger.dev task receives a file that isn't publicly accessible by URL, upload it to FFmpeg Micro first using the presigned upload flow:
// 1. Get a presigned upload URL
const presignRes = await fetch(`${BASE_URL}/v1/upload/presigned-url`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
filename: "input.mp4",
contentType: "video/mp4",
fileSize: fileBuffer.byteLength,
}),
});
const { result } = await presignRes.json();
// 2. PUT the file to the presigned URL
await fetch(result.uploadUrl, {
method: "PUT",
headers: { "Content-Type": "video/mp4" },
body: fileBuffer,
});
// 3. Confirm the upload
const confirmRes = await fetch(`${BASE_URL}/v1/upload/confirm`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
filename: result.filename,
fileSize: fileBuffer.byteLength,
}),
});
const confirmed = await confirmRes.json();
// Now use confirmed.result.gcsUrl as your input URL for transcoding
The upload goes directly to cloud storage, not through the API server. Fast even for large files.
Common pitfalls
Checking only for "completed" in your poll loop. If a job fails and you're not checking for "failed", your loop runs until the Trigger.dev task times out. Always check both terminal states.
Mixing options and preset in the same request. If you include both, options takes priority and preset is ignored. Pick one per request.
Storing the signed download URL for later use. Signed URLs expire after 10 minutes. Always call the /download endpoint fresh when you actually need the file.
Using -filter_complex instead of -vf. The FFmpeg Micro API doesn't support -filter_complex. Use -vf for video filter operations.
Polling too aggressively. A 1-second interval wastes API calls. Five seconds works well for most video lengths. A 10-minute 1080p file typically processes in 1-3 minutes.
FAQ
Do I still need @trigger.dev/build for this approach?
No. The API approach uses standard fetch calls. Remove ffmpegExtension() from your trigger.config.ts entirely. No build extensions are needed.
What if my Trigger.dev task times out before the transcode finishes?
Increase the maxDuration on your task definition. Most transcodes finish within a few minutes. For very long videos, consider splitting the polling into a separate task that the first task triggers, or use Trigger.dev's wait utilities.
Can I process videos that require authentication to download?
Yes. Use a pre-signed URL from your storage provider (S3 pre-signed URL, GCS signed URL) as the input URL. FFmpeg Micro downloads the video from whatever URL you provide. For more details, see our guide on FFmpeg authorization headers.
How does pricing compare to running my own FFmpeg binary?
The binary approach costs you compute time on your Trigger.dev infrastructure, plus the cold-start overhead on every invocation. FFmpeg Micro has a free tier with 10 minutes of processing per month, enough to build and test your workflow. Paid plans scale based on usage. For a deeper comparison, see self-hosting FFmpeg vs. using an API.
What video formats are supported?
Input: any format FFmpeg can read, including MP4, WebM, MOV, AVI, and MKV. Output: mp4, webm, or mov. Audio formats (MP3, WAV, AAC) are supported for audio extraction workflows.
If you're already using Trigger.dev for background jobs and want video processing without the binary overhead, grab a free FFmpeg Micro API key and swap out the extension. The TypeScript examples above work as-is. For more on using FFmpeg with TypeScript in general, check out our FFmpeg with TypeScript guide.
*Last verified: July 2026. Code examples tested against Trigger.dev SDK v3 and FFmpeg Micro API v1.*
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

How to Use FFmpeg with Pipedream (No Timeout Errors, No Binary Setup)
Pipedream can't run FFmpeg natively due to 30-second timeouts and missing binaries. Use FFmpeg Micro's REST API for reliable video processing in Pipedream workflows.

How to Call FFmpeg from n8n Without the Execute Command Node
Replace the n8n Execute Command node with HTTP Request calls to a cloud FFmpeg API. Works on n8n Cloud and self-hosted with zero dependencies.

How to Use FFmpeg with TypeScript (No Installation Required)
Learn how to process video in TypeScript without installing FFmpeg. Compare child_process, fluent-ffmpeg, ffmpeg.wasm, and cloud API approaches with typed code.
Ready to process videos at scale?
Start using FFmpeg Micro's simple API today. No infrastructure required.
Get Started Free