Genace

Video generation API retry backoff: retry reads without multiplying renders

Video generation API retry backoff needs an operation boundary. Retrying a status read usually continues observing one existing job. Retrying a generation POST can create another paid job. Do not put both operations behind the same generic retry decorator and assume they have equivalent consequences.

Genace’s internal scheduled polling uses a five-second base, exponential growth, and a sixty-second cap. That is server-side worker scheduling. It is separate from your application’s retry policy and separate from the two-second refresh interval inside a long-poll request.

Classify before delaying

ResultRetry policy
Network error during GETRetry the same job read with a bounded delay
Rate-limit responseReduce aggregate traffic and wait
Concurrency-limit responseWait for an active job to finish
Invalid parametersCorrect the request before another submission
Insufficient creditsResolve the budget condition
Terminal failed jobInspect the job error before authorizing new work
Lost submission responseReconcile the ambiguous attempt

This classification matters more than the exact backoff formula. Sleeping longer cannot repair an unsupported duration, and a fast retry cannot create more balance. Similarly, a valid failed-job snapshot is not a transient HTTP failure just because your product wanted success.

Use bounded jitter for status reads

The following Node.js example retries only a GET. Its delays and attempt count are application choices. It does not assume the API sends a Retry-After header, because the current error serializer does not add one.

async function readWithBackoff(jobId) {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    try {
      const response = await fetch(
        `https://genace.ai/api/v1/jobs/${encodeURIComponent(jobId)}?wait=0`,
        {
          headers: { Authorization: `Bearer ${process.env.GENACE_API_KEY}` },
          signal: AbortSignal.timeout(15000),
        }
      );
      if (response.ok) return await response.json();
      if (![429, 500, 502, 503, 504].includes(response.status)) {
        return { readRejected: response.status, body: await response.json() };
      }
    } catch (error) {
      if (attempt === 4) throw error;
    }
    const ceiling = Math.min(60000, 2000 * 2 ** attempt);
    const delay = Math.floor(ceiling / 2 + Math.random() * ceiling / 2);
    await new Promise((resolve) => setTimeout(resolve, delay));
  }
  return { pendingRead: true, jobId };
}

Jitter spreads workers across a window instead of waking every client at the same instant. A bounded attempt count prevents one request from keeping a serverless invocation alive indefinitely. Persist the job ID when the local retry budget ends so another invocation can continue later.

Coordinate the whole key

Independent backoff loops can still overload a shared key. If ten workers all receive throttling, reduce the shared dispatch rate instead of merely giving each worker its own timer. The current rate limiter records rejected requests too, so repeatedly probing during a saturated window can prolong pressure.

Verification checklist

Use mocked read responses to verify that validation and authentication failures stop immediately, retryable reads stay on one identifier, and exhaustion produces a resumable outcome. Simulate several workers and inspect whether their delays spread out. Confirm that no retry helper contains a hidden generation POST.

Related material includes 429 errors, duplicate requests, and polling.

Continue with production operations

Where these facts come from

  • codebase: src/ai/jobs/backoff.ts
  • codebase: src/ai/api/errors.ts
  • codebase: src/ai/api/rate-limit.ts
  • Editorial keyword hypothesis; search volume not measured