Guides
Handling failures

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.

StatusCauseResponse
400Malformed JSON, missing or non-URL inputUrl, missing preset, unknown presetFix the request. Retrying identical input gives the same answer
401Key missing, unknown or revokedFix the credential
403Preset exists but is not on your planUse one from availablePresets, or upgrade
429Monthly quota exhaustedWait for the month to roll over or change plan
500Our faultRetry 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.

PlanLongest source
Free10 minutes
Pro60 minutes
Business180 minutes
Enterprise480 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.