DEV Community

Cover image for Test ElevenLabs 429 Handling Without Burning API Quota
FetchSandbox
FetchSandbox

Posted on Originally published at fetchsandbox.com

Test ElevenLabs 429 Handling Without Burning API Quota

Your app loads the ElevenLabs voice list successfully. The user picks a voice, clicks Generate, and the text-to-speech request returns 429.

That is a more useful test than an API that always rate-limits. Authentication worked. Discovery worked. The failure happened on the expensive operation, after the UI had already promised progress.

The branch most mocks skip

A generated ElevenLabs integration usually proves this sequence:

GET  /v1/voices                    -> 200
POST /v1/text-to-speech/{voiceId}  -> 200
Enter fullscreen mode Exit fullscreen mode

The fixture is green forever. It does not answer:

  • What happens when voice discovery succeeds but generation is throttled?
  • Does the client retry immediately?
  • Does the SDK already retry before your wrapper retries?
  • Does the UI create several concurrent generation jobs?
  • Is there a maximum attempt count?
  • What happens when no Retry-After header is present?

Those are client-policy questions. Burning production quota until ElevenLabs returns 429 is an expensive way to discover the answers.

A 429 is not permission to retry forever

Rate-limit handling needs a decision function, not a recursive catch:

type RetryDecision =
  | { action: "wait"; delayMs: number }
  | { action: "stop"; reason: string };

function decide429(
  response: Response,
  attempt: number,
  remainingMs: number
): RetryDecision {
  if (attempt >= 3) {
    return { action: "stop", reason: "retry budget exhausted" };
  }

  const retryAfter = response.headers.get("retry-after");
  const fallbackMs = Math.min(1000 * 2 ** attempt, 8000);
  const delayMs = retryAfter
    ? parseRetryAfter(retryAfter)
    : fallbackMs;

  if (delayMs > remainingMs) {
    return { action: "stop", reason: "deadline exceeded" };
  }

  return { action: "wait", delayMs };
}
Enter fullscreen mode Exit fullscreen mode

The important assertions are:

generation_attempts <= 3
voice_list_calls == 1
no duplicate UI jobs
fallback backoff used when Retry-After is absent
controlled error returned when the retry budget ends
Enter fullscreen mode Exit fullscreen mode

Do not assert only that the client eventually threw. A tight loop can throw after hammering the provider twenty times.

Watch for stacked retries

Many SDKs retry transient failures internally. If your application wrapper also retries, the budgets multiply:

SDK attempts:          3
application attempts:  3
actual HTTP requests:  9
Enter fullscreen mode Exit fullscreen mode

That is why the test needs a request count. Pick one layer to own the policy, or coordinate the limits explicitly.

The failure can get worse in a browser. A disabled button that re-enables too early allows the user to start another retry tree while the first is waiting.

Force the real workflow shape without an API key

FetchSandbox ships an ElevenLabs twin with a curated text_to_speech workflow:

  1. GET /v1/voices to discover available voices
  2. Select the returned voice_id
  3. POST /v1/text-to-speech/{voice_id}

Under the rate_limited scenario, only the textToSpeech operation changes:

rate_limited:
  overrides:
    - operation_id: textToSpeech
      response_status: 429
      error_code: rate_limit_exceeded
      error_detail: "Rate limit exceeded. Please try again later."
Enter fullscreen mode Exit fullscreen mode

The first step still succeeds. The generation step returns the provider-shaped 429. The configured response intentionally has no Retry-After, so this scenario tests your documented fallback backoff and retry ceiling rather than server-directed waiting.

No ElevenLabs API key and no quota consumption are involved.

With FetchSandbox MCP in Cursor or Claude:

Run elevenlabs text_to_speech under rate_limited.
Verify voice discovery succeeds and generation returns 429.
Count every generation attempt, prove the voice list is not fetched
again, and confirm the client stops at its retry budget.
Give me the run receipt.
Enter fullscreen mode Exit fullscreen mode

The receipt provides the request sequence and exact failure. Your app-level test supplies the clock and UI assertions.

Make backoff deterministic

Do not make CI literally sleep through production delays. Inject time:

type RetryClock = {
  now(): number;
  sleep(ms: number): Promise<void>;
  random(): number;
};
Enter fullscreen mode Exit fullscreen mode

Production uses the real clock. Tests use a fake clock and record requested delays:

attempt 1 -> 429 -> requested sleep 1000ms
attempt 2 -> 429 -> requested sleep 2000ms
attempt 3 -> 429 -> stop
Enter fullscreen mode Exit fullscreen mode

You can prove exponential backoff, jitter bounds, cancellation, and the total deadline without adding seconds to every CI run.

Local first, pipeline next

Ask the coding agent to run the twin while it writes the policy. Then keep the same scenario in CI:

fetchsandbox run <sandbox-id> text_to_speech \
  --scenario rate_limited \
  --json
Enter fullscreen mode Exit fullscreen mode

The local receipt is useful during review. The pipeline prevents a future refactor from changing maxAttempts = 3 into unbounded recursion.

Questions people ask

How can I test ElevenLabs rate limits without using quota?

Point the integration at a service twin and force textToSpeech to return 429. This exercises the client contract without sending audio-generation requests to ElevenLabs.

Should I always use Retry-After?

Honor a valid Retry-After when the provider sends one, subject to your own deadline and attempt cap. When it is absent, use a documented capped backoff with jitter. Test both branches separately.

Should a 429 restart the whole workflow?

No. Resume from the safe failed operation. In this workflow, do not fetch voices again or create a second local generation job merely because text-to-speech was throttled.

Is rate limiting the same as exhausted quota?

No. A temporary rate limit is generally pause-and-retry. A hard quota or billing limit is stop-and-alert. OpenAI, for example, can use HTTP 429 for both while distinguishing them with error codes such as rate_limit_exceeded and insufficient_quota.

Does FetchSandbox replace ElevenLabs testing?

No. Use the twin for repeatable negative paths and CI. Use ElevenLabs afterward for final credentials, model availability, audio quality, and real account limits.

The evidence to attach

Require a receipt showing one successful voice lookup followed by the forced 429, plus an app assertion showing the exact retry count and requested delays.

The canonical guide covers mid-workflow 429 testing without partner quota. Connect the twins through FetchSandbox MCP.

Top comments (0)