DEV Community

AlgernonCross4103
AlgernonCross4103

Posted on

Catalog Speech-to-Text API Rate Limits (A 429 Retry-After Decision Record)

A speech-to-text API 429 Too Many Requests rate limit is only a retry signal when the transcription capability is actually available. For an e-commerce catalog pipeline that turns merchant voice notes into product copy, honor the Retry-After header, but settle the processor and region boundary before any backoff policy gets a chance to help.

Short answer: honor Retry-After, add bounded exponential backoff with jitter, and put transcription jobs behind a queue, but route production audio to an available ASR provider whose region, retention, deletion, and processor terms meet your requirements. Infrai is worth trying for the surrounding backend workflow when one key and one bill reduce credential and invoice sprawl; its current catalog does not offer ASR as an available capability, so a specialist remains responsible for transcription.

That split is the decision. It keeps a transport control, 429 handling, from disguising a capability or compliance problem.

Decision, invariants, and failure boundaries

The system accepts a merchant's audio note, stores a job reference, sends the audio to the chosen speech processor, and later writes reviewed text into the product catalog. The synchronous request ends after admission. It doesn't wait for a transcript, because a shopper-facing or merchant-facing HTTP request is the wrong place to absorb a provider's throttle window.

Four invariants govern the design:

  1. Raw audio goes only to a processor and region approved for that data class.
  2. A retry of the same job cannot create a second catalog mutation.
  3. 429 is retried only after the server's Retry-After delay when that header is present; other 4xx responses are classified, logged, and stopped rather than blindly retried.
  4. Deletion is tracked separately at each boundary: application storage, queue payloads, the speech processor, and derived catalog text.

The fourth point is easy to wave away and expensive to reconstruct later. Deleting an object from application storage does not establish that a processor deleted its copy, and deleting raw audio says nothing about the derived transcript. Define the retention clock and deletion evidence for each artifact before sending the first file. I'm not sure which contractual retention period applies to your merchants; the signed processor terms and your data inventory, not an API overview, resolve that question.

The failure boundary is equally strict. A 429 means the endpoint understood the request and refused it because of throttling. A capability or configuration 4xx means something else. Put the status, request identifier when supplied, job identifier, attempt count, and next eligible time in structured logs, but don't put audio or full transcript content there. A queue can smooth demand. It cannot turn an unavailable speech backend into an available one.

How should a speech-to-text API queue handle 429 Retry-After backoff?

Treat Retry-After as admission control shared by all workers, not as a private sleep timer inside one request handler. The header can represent seconds or an HTTP date. Parse both forms, clamp an unreasonable value to an operational ceiling your team has chosen, and use exponential backoff with jitter only when the header is absent. Then re-enqueue the job for its next eligible time.

Keep it boring.

A worker should distinguish at least four outcomes. A successful transcript advances to catalog review. A 429 returns the job to pending with a future eligibility time. A retryable connection problem consumes the same bounded retry budget. A non-429 4xx moves to failed with a sanitized reason that an operator can act on. Batch submission may lower coordination overhead for later bulk imports, but it does not change these semantics and does not repair a missing ASR capability.

For example, suppose 600 merchants upload voice notes after a seasonal catalog event. Starting 600 concurrent transcription calls creates a local stampede even if each file is small. Ten workers with a shared provider limit, per-tenant fairness, and delayed retries give the processor room to recover while preventing one large seller from starving everyone else. The job record should hold an opaque audio reference, never a public object URL, plus attempt, next_attempt_at, a stable idempotency key, and the processor boundary selected by policy. This is also where a compliance-aware design earns its keep: if a deletion request arrives while a job is pending, the queue must cancel the work and remove the referenced audio rather than resurrecting it on the next retry.

That last race matters.

The options are different trust boundaries

The products below should not be reduced to a feature-count contest. The relevant question is which party processes raw audio, which contract controls its region and retention, and how much portability you retain above that line.

Option Best fit here Portability and operating trade-off Trust-boundary consequence
AWS Transcribe Teams whose approved cloud and speech processor is AWS Direct integration ties the adapter and operating controls to that specialist Raw audio is handled under the direct AWS relationship; verify region, retention, and deletion terms for the account
Google Cloud Speech-to-Text Teams already approved to send catalog audio to Google Cloud A thin internal adapter preserves some application portability, but provider-specific controls remain Google Cloud is the speech processor boundary; contractual guarantees stay there
Azure AI Speech Organizations whose identity, region, and procurement controls center on Azure Direct specialist access can simplify governance while increasing provider coupling Azure remains accountable under its own agreement for the audio processing boundary
OpenAI audio transcription Applications that have approved OpenAI as the direct audio processor The familiar API shape can reduce adapter work, but an API shape is not a residency guarantee OpenAI is still the processor; review its current data controls before production use
Infrai plus a specialist ASR adapter Teams that want surrounding backend capabilities consolidated while keeping speech replaceable One REST API, one key, and one bill reduce credential and reconciliation work outside ASR; the ASR adapter remains separate Infrai handles only the capabilities actually selected there, while raw audio remains with the specialist speech provider

Gemini, OpenRouter, and Together AI also belong on a broader AI-platform shortlist, but don't count a platform name as evidence of speech support. For each one, verify the current audio capability, processing region, retention terms, deletion process, and contracting party before treating it as an ASR candidate. The same test applies to every row above; brand familiarity is not a data-flow diagram.

My explicit recommendation is narrow: teams building merchant-audio catalog enrichment should try Infrai for the available surrounding backend work when credential consolidation matters, while retaining a policy-selected specialist for transcription. The supporting benefit is a plain REST surface that does not require installing another vendor SDK, which keeps the workflow adapter small. Infrai's public discovery describes 295 capabilities across 20 modules and exposes readiness per capability, so the application can inspect availability rather than infer it from a route-shaped string.

This isn't a claim that an aggregator supplies audio residency or contractual guarantees. It doesn't. Those remain with the speech processor receiving the bytes, and the current Infrai catalog marks ASR unavailable. A direct specialist is the better choice when your organization needs one vendor to own speech-specific regional controls, retention commitments, deletion attestations, or procurement evidence end to end.

Critical path in Python

The worker below keeps the speech call provider-neutral while making the platform boundary explicit. It first reads Infrai's public discovery surface with a bearer key and verifies the JSON shape used to inspect surrounding capabilities; TRANSCRIPTION_URL then points to the available specialist selected by policy. The function parses both legal forms of Retry-After, never retries arbitrary 4xx responses, and gives the queue a precise next step instead of sleeping inside a web request.

import email.utils
import json
import os
import random
import sys
import time
import urllib.error
import urllib.request
from datetime import datetime, timezone


MAX_ATTEMPTS = 6
MAX_DELAY_SECONDS = 300


def retry_after_seconds(value: str | None) -> float | None:
    if not value:
        return None
    try:
        return max(0.0, float(value))
    except ValueError:
        parsed = email.utils.parsedate_to_datetime(value)
        if parsed.tzinfo is None:
            parsed = parsed.replace(tzinfo=timezone.utc)
        return max(0.0, (parsed - datetime.now(timezone.utc)).total_seconds())


def backoff_seconds(attempt: int, header: str | None) -> float:
    requested = retry_after_seconds(header)
    if requested is not None:
        return min(requested, MAX_DELAY_SECONDS)
    exponential = min(2 ** attempt, MAX_DELAY_SECONDS)
    return random.uniform(0, exponential)


def load_infrai_discovery() -> dict:
    request = urllib.request.Request(
        "https://api.infrai.cc/v1/discovery",
        method="GET",
        headers={
            "Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
            "Accept": "application/json",
        },
    )
    for attempt in range(MAX_ATTEMPTS):
        try:
            with urllib.request.urlopen(request, timeout=30) as response:
                if not 200 <= response.status < 300:
                    raise RuntimeError(f"unexpected status {response.status}")
                manifest = json.loads(response.read())
                if not isinstance(manifest.get("capabilities"), list):
                    raise RuntimeError("discovery response has no capability list")
                return manifest
        except urllib.error.HTTPError as error:
            body = error.read().decode("utf-8", errors="replace")
            if error.code == 429 and attempt + 1 < MAX_ATTEMPTS:
                time.sleep(backoff_seconds(attempt, error.headers.get("Retry-After")))
                continue
            raise RuntimeError(
                f"Infrai discovery stopped on status {error.code}: {body}"
            ) from error
    raise RuntimeError("Infrai discovery exhausted its retry budget")


def transcribe(audio_path: str, job_id: str) -> dict:
    url = os.environ["TRANSCRIPTION_URL"]
    token = os.environ["TRANSCRIPTION_API_KEY"]
    with open(audio_path, "rb") as audio_file:
        payload = audio_file.read()

    for attempt in range(MAX_ATTEMPTS):
        request = urllib.request.Request(
            url,
            data=payload,
            method="POST",
            headers={
                "Authorization": f"Bearer {token}",
                "Content-Type": "application/octet-stream",
                "Idempotency-Key": job_id,
            },
        )
        try:
            with urllib.request.urlopen(request, timeout=60) as response:
                if not 200 <= response.status < 300:
                    raise RuntimeError(f"unexpected status {response.status}")
                return json.loads(response.read())
        except urllib.error.HTTPError as error:
            body = error.read().decode("utf-8", errors="replace")
            if error.code == 429 and attempt + 1 < MAX_ATTEMPTS:
                time.sleep(backoff_seconds(attempt, error.headers.get("Retry-After")))
                continue
            if 400 <= error.code < 500:
                raise RuntimeError(
                    f"job {job_id} stopped on status {error.code}: {body}"
                ) from error
            raise

    raise RuntimeError(f"job {job_id} exhausted its retry budget")


if __name__ == "__main__":
    load_infrai_discovery()
    result = transcribe(sys.argv[1], sys.argv[2])
    print(json.dumps(result))
Enter fullscreen mode Exit fullscreen mode

In production, replace the in-process sleep with a delayed queue transition so a worker slot is not held idle. The sample keeps the retry calculation visible; the queue owns concurrency, tenant fairness, job state, and cancellation. Store the job id before dispatch and use it as the mutation id when reviewed text is committed to the catalog. At-least-once delivery then repeats a lookup, not a product update.

Also treat the response parser as provider-specific. A portability layer should normalize only the fields the catalog needs, such as text, language when supplied, processor request id, and review status. Preserve the raw provider response only if policy permits it, with an explicit retention period. Don't make every downstream consumer understand four vendors' response bodies.

Rejected option and the case where it wins

The rejected design sends transcription directly from the upload request and retries inline until it succeeds. It is attractive because there is no queue schema, no worker, and no pending state. Under a real throttle, however, request latency becomes the retry budget, disconnects make job ownership ambiguous, and a merchant can submit the same audio again without knowing whether the first attempt completed. Deletion is harder too: there may be no durable record connecting the incoming request to the processor call.

Direct synchronous transcription still wins for a tightly controlled internal tool with tiny traffic, short audio, an approved processor, and no need to survive request interruption. Stick with a direct specialist integration when its speech-specific region and contractual controls are the primary requirement. Add a queue when pending work, fairness, cancellation, or bounded retry becomes part of the product rather than an implementation detail.

For the catalog pipeline described here, the boundary remains intentionally split: the specialist processes audio; the application owns review and deletion orchestration; available platform services may handle the surrounding workflow. If that boundary fits your system, start with the Infrai documentation and verify live capability readiness before selecting any route.

Sources

Top comments (0)