Handling failures
Failures happen in two places: when you create the job, and later while it is being processed. They need different handling because the first is your bug and the second usually is not.
At creation
The API answers immediately. Everything it can check without touching your file it checks now.
| Status | Cause | Response |
|---|---|---|
400 | Malformed JSON, missing or non-URL inputUrl, missing preset, unknown preset | Fix the request. Retrying identical input gives the same answer |
401 | Key missing, unknown or revoked | Fix the credential |
403 | Preset exists but is not on your plan | Use one from availablePresets, or upgrade |
429 | Monthly quota exhausted | Wait for the month to roll over or change plan |
500 | Our fault | Retry with backoff, quoting requestId if it persists |
What creation does not check is your file. The source URL is not fetched,
its format is not inspected, and its duration is not measured. All of that
happens later, in the worker. A 201 means the job is queued, not that it will
succeed.
During processing
Processing failures land on the job itself: status becomes failed and
errorMessage says why. If you have a webhook configured, you are told.
{
"jobId": "9d1f5c0a-4e7b-4a61-9b2e-8f1c3d5a7e42",
"status": "failed",
"error": "DURATION_EXCEEDED: Video duration (24 minutes) exceeds plan limit (10 minutes)",
"preset": "mp4_264_720p",
"timestamp": "2026-09-10T14:11:02.664Z"
}Source too long
The worker downloads the file and measures it with ffprobe. If it is longer
than your plan allows, the job fails with DURATION_EXCEEDED and is not
retried, because retrying cannot change the length of the file.
| Plan | Longest source |
|---|---|
| Free | 10 minutes |
| Pro | 60 minutes |
| Business | 180 minutes |
| Enterprise | 480 minutes |
This failure still consumes a job from your monthly quota. The slot is claimed when the job is created, and it is not returned when processing fails. If your users upload arbitrary files, measure duration on your side before submitting.
Source unreachable
The URL has to be publicly reachable by our workers over HTTP or HTTPS at the moment the job runs, which may be minutes after you created it.
Common causes are links that require authentication, links that have already expired, private or internal addresses, and object storage that is not public. The worker retries these up to three times with exponential backoff, so a transient outage recovers on its own. A permanently wrong URL exhausts the retries and fails.
If your source is private, generate your own signed URL with a lifetime that comfortably outlasts the queue, and pass that.
Source cannot be encoded
A corrupt file, a container we cannot demux, or a codec combination the preset
cannot produce fails with FFmpeg's own wording in errorMessage. Retried three
times, which rarely helps for this class, then failed.
Stuck jobs
A job whose worker died is swept up and marked failed with
Job stuck in processing state - marked as failed by cleanup. Treat it like
any other failure and resubmit.
Retries, ours and yours
Our worker retries a failed job up to three times with exponential backoff
starting at two seconds, except for DURATION_EXCEEDED. retryCount on the
job tells you how many attempts it took. All of that happens inside one job and
costs you one quota slot.
By the time you see status: "failed", our retries are finished. Resubmitting
is your decision and it creates a new job, which costs another slot. Only
resubmit when you have changed something: a reachable URL, a shorter source, a
different preset.
A handler that covers the cases
function classify(job) {
const message = job.errorMessage ?? ''
if (message.includes('DURATION_EXCEEDED')) return 'too_long'
if (/download|ENOTFOUND|ECONNREFUSED|404/i.test(message)) return 'unreachable'
if (/stuck in processing/i.test(message)) return 'infrastructure'
return 'unencodable'
}
async function onFailed(job) {
switch (classify(job)) {
case 'too_long':
return tellUser('That video is longer than your plan allows.')
case 'unreachable':
return tellUser('We could not download that file. Check the link is public and current.')
case 'infrastructure':
return resubmit(job) // safe to try again unchanged
case 'unencodable':
return tellUser('That file could not be processed. Try re-exporting it as MP4.')
}
}Match on errorMessage text with care. It is written for people and is not a
stable machine contract; only DURATION_EXCEEDED is a fixed token.
Webhook delivery failures
If your endpoint is down, the notification is lost, not the job. We try three times and stop; there is no dead letter queue and no manual replay.
Reconcile rather than relying on delivery alone. Any job your database still
believes is processing after its estimated time has passed is worth reading
from the API directly.