RenderIO
Concepts

Async Processing

RenderIO processes all FFmpeg commands asynchronously. Learn about the submit-and-poll model, command statuses, and how to retrieve results.

Async Processing

RenderIO is fully asynchronous. When you submit an FFmpeg command, the API validates your request and returns a command_id immediately. The actual FFmpeg processing happens in the background. You never wait for FFmpeg to finish on the HTTP connection.

The submit-and-poll model

Every command follows this lifecycle:

  Submit command
       |
       v
  Receive command_id  (HTTP response, typically < 100ms)
       |
       v
  FFmpeg runs in the background
       |
       v
  Retrieve results via polling or webhook

There are two ways to get results:

  1. Polling - periodically call GET /api/v1/commands/{command_id} until the status is SUCCESS or FAILED
  2. Webhooks - configure a webhook URL and RenderIO will POST the result to your server when processing completes

Command statuses

Every command moves through these statuses:

StatusDescription
QUEUEDCommand accepted and waiting to be processed
PROCESSINGFFmpeg is actively running in a sandbox
SUCCESSCompleted successfully, output files are available
FAILEDAn error occurred, check error_status and error_message

Status transitions always move forward: QUEUED -> PROCESSING -> SUCCESS or FAILED. A command never moves backward.

Polling for results

After submitting a command, poll the status endpoint with the returned command_id:

curl https://renderio.dev/api/v1/commands/550e8400-e29b-41d4-a716-446655440000 \
  -H "X-API-KEY: ffsk_your_api_key"

A successful response looks like:

{
  "command_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "SUCCESS",
  "command_type": "FFMPEG_COMMAND",
  "total_processing_seconds": 4.2,
  "ffmpeg_command_run_seconds": 3.1,
  "output_files": {
    "out_video": {
      "file_id": "...",
      "storage_url": "https://storage.renderio.dev/...",
      "status": "STORED",
      "filename": "output.mp4",
      "size_mbytes": 12.4,
      "duration": 30.5,
      "width": 1920,
      "height": 1080,
      "codec": "h264"
    }
  },
  "original_request": {
    "input_files": { "in_video": "https://example.com/input.mp4" },
    "output_files": { "out_video": "output.mp4" },
    "ffmpeg_command": "-i {{in_video}} -c:v libx264 {{out_video}}"
  }
}

Polling best practices

  • Start at 1-second intervals for short jobs (< 30 seconds expected)
  • Use exponential backoff for longer jobs: poll at 1s, 2s, 4s, 8s, etc.
  • Set a maximum poll duration to avoid polling indefinitely
  • Check for terminal states: stop polling once the status is SUCCESS or FAILED

Here is a polling example with exponential backoff:

COMMAND_ID="550e8400-e29b-41d4-a716-446655440000"
DELAY=1
MAX_DELAY=30
TIMEOUT=300
START=$(date +%s)

while true; do
  NOW=$(date +%s)
  ELAPSED=$((NOW - START))
  if [ $ELAPSED -ge $TIMEOUT ]; then echo "Polling timed out"; exit 1; fi

  RESULT=$(curl -s https://renderio.dev/api/v1/commands/$COMMAND_ID \
    -H "X-API-KEY: $RENDERIO_API_KEY")
  STATUS=$(echo $RESULT | jq -r '.status')
  echo "Status: $STATUS"

  if [ "$STATUS" = "SUCCESS" ] || [ "$STATUS" = "FAILED" ]; then break; fi
  sleep $DELAY
  DELAY=$((DELAY * 2))
  if [ $DELAY -gt $MAX_DELAY ]; then DELAY=$MAX_DELAY; fi
done

echo $RESULT | jq .
import time
import requests

def poll_command(command_id, api_key):
    delay = 1  # Start at 1 second
    max_delay = 30  # Cap at 30 seconds
    timeout = 300  # Give up after 5 minutes
    start = time.time()

    while time.time() - start < timeout:
        response = requests.get(
            f"https://renderio.dev/api/v1/commands/{command_id}",
            headers={"X-API-KEY": api_key}
        )
        data = response.json()

        if data["status"] in ("SUCCESS", "FAILED"):
            return data

        time.sleep(delay)
        delay = min(delay * 2, max_delay)

    raise TimeoutError("Polling timed out")
interface CommandResult {
  command_id: string;
  status: "QUEUED" | "PROCESSING" | "SUCCESS" | "FAILED";
  output_files?: Record<string, { file_id: string; storage_url: string }>;
}

async function pollCommand(commandId: string, apiKey: string): Promise<CommandResult> {
  let delay = 1000;
  const maxDelay = 30000;
  const timeout = 300000;
  const start = Date.now();

  while (Date.now() - start < timeout) {
    const response = await fetch(
      `https://renderio.dev/api/v1/commands/${commandId}`,
      { headers: { "X-API-KEY": apiKey } }
    );
    const data = (await response.json()) as CommandResult;

    if (data.status === "SUCCESS" || data.status === "FAILED") {
      return data;
    }

    await new Promise((resolve) => setTimeout(resolve, delay));
    delay = Math.min(delay * 2, maxDelay);
  }

  throw new Error("Polling timed out");
}
async function pollCommand(commandId, apiKey) {
  let delay = 1000;
  const maxDelay = 30000;
  const timeout = 300000;
  const start = Date.now();

  while (Date.now() - start < timeout) {
    const response = await fetch(
      `https://renderio.dev/api/v1/commands/${commandId}`,
      { headers: { "X-API-KEY": apiKey } }
    );
    const data = await response.json();

    if (data.status === "SUCCESS" || data.status === "FAILED") {
      return data;
    }

    await new Promise((resolve) => setTimeout(resolve, delay));
    delay = Math.min(delay * 2, maxDelay);
  }

  throw new Error("Polling timed out");
}
<?php
function pollCommand(string $commandId, string $apiKey): array {
    $delay = 1;
    $maxDelay = 30;
    $timeout = 300;
    $start = time();

    while (time() - $start < $timeout) {
        $ch = curl_init("https://renderio.dev/api/v1/commands/$commandId");
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-API-KEY: $apiKey"]);
        $data = json_decode(curl_exec($ch), true);
        curl_close($ch);

        if (in_array($data["status"], ["SUCCESS", "FAILED"])) {
            return $data;
        }

        sleep($delay);
        $delay = min($delay * 2, $maxDelay);
    }

    throw new Exception("Polling timed out");
}

Using webhooks instead of polling

If you prefer push-based notifications, configure a webhook URL either globally (via the webhook config endpoint) or per-command (via the webhook_url field in your request body). When the command completes, RenderIO sends a POST request to your URL with the full command result.

curl -X POST https://renderio.dev/api/v1/run-ffmpeg-command \
  -H "X-API-KEY: ffsk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "input_files": { "in_video": "https://example.com/input.mp4" },
    "output_files": { "out_video": "output.mp4" },
    "ffmpeg_command": "-i {{in_video}} -c:v libx264 {{out_video}}",
    "webhook_url": "https://your-server.com/webhooks/renderio"
  }'

The webhook payload contains the same data as a poll response, wrapped with a timestamp:

{
  "data": {
    "command_id": "...",
    "status": "SUCCESS",
    "output_files": { ... }
  },
  "timestamp": 1700000000000
}

Timeouts

Command timeout is determined by your subscription plan. If FFmpeg does not finish within this window, the command is marked as FAILED.

Background execution

Under the hood, RenderIO uses Cloudflare Workers' waitUntil to run FFmpeg processing in the background. This is why your initial HTTP request returns in milliseconds -- the response is sent before processing begins. The Worker continues running the sandbox, file downloads, FFmpeg execution, and file uploads after your connection closes.

On this page