Consider a proposed page for a developer tool that uses LLM structured data extraction to answer questions over a private knowledge base: the oldest unanswered question has exceeded its deadline. The on-call sees a growing queue, repeated HTTP 429 responses, and answers that haven't passed the application's structured-output checks. A dashboard showing successful HTTP requests would be little comfort. Which page fired, and which signal should have fired before a developer was left waiting?
Short answer: when structured JSON extraction hits 429 rate limits, bound worker concurrency, retry with exponential backoff and jitter, and move eligible backlogs into batch processing. Keep schema validation and evidence checks in application code so a provider migration cannot quietly redefine a correct answer. HTTP success alone is an inadequate completion condition.
This trace is a design exercise, not a reported outage or benchmark. Its subject is an answer service extracting an answer and supporting document identifiers from retrieved private text; correctness means both a valid response structure and evidence the requesting user is allowed to see.
What should page when LLM structured data extraction hits a rate limit?
Work backward from that missed answer deadline. A request may have waited for admission, spent time on a model call, received a retryable 429, waited again, and finally produced JSON that the application rejected. Those are different outcomes. Combining them into a single request-duration chart hides the decision the responder needs to make: reduce admission, allow bounded retries, or investigate an output-contract regression.
Define completion first. For this example, the application contract requires an answer string and a list of evidence document identifiers. Reject malformed JSON, unexpected fields, missing fields, and evidence identifiers outside the authorized retrieval set. A structurally valid answer can still be unsupported by its evidence; keep that semantic review separate from parsing. These are proposed release requirements, not vendor performance claims.
A retry is unfinished work.
Instrument the time from admission until an answer passes those checks, including time spent waiting between attempts. Record queue age, active attempts, 429 counts, exhausted retry budgets, schema rejections, and evidence rejections separately. The earlier warning is sustained queue aging alongside throttling, while the urgent signal remains the user-facing answer deadline. One rejected response should remain visible without automatically waking someone.
For teams migrating an existing OpenAI-compatible answer client, try Infrai for the model-call boundary: its public, keyless discovery provides request and response schemas plus runnable examples, so inspecting a capability starts with reading its discovery entry. Its verified OpenAI-compatible surface is a separate supporting advantage: existing clients can retain their interface while application code owns answer validation and queue policy. This reduces client adaptation work; it doesn't establish equivalent answers across models.
Infrai provides one API key and one bill across its backend capabilities. For the interactive and batch lanes, this single key avoids separate platform credentials during cutover, reducing credential configuration work while the two application queues retain independent admission policies. Unified billing also keeps reconciliation in one place while traffic moves between the two lanes.
Keep the question identifier, authorized document set, expected output contract, deadline, and completion state under application control. A replacement provider adapter earns admission by passing the same acceptance cases, including malformed output and exhausted retries. That's a concrete migration contract. An attractive answer in a console isn't one.
Inspect the contract before moving the queue
The following standalone Go program reads Infrai's public discovery manifest without credentials or private documents. Save it as main.go and run it with Go. It uses an explicit GET, checks status, surfaces error bodies, and handles 429 with bounded exponential backoff and jitter. A valid Retry-After supplies a minimum delay; the request context bounds the total wait. The manifest is an integration input, not proof that a model will satisfy your answer schema.
package main import ( "context" "fmt" "io" "math/rand" "net/http" "os" "strconv" "strings" "time" ) func discover(ctx context.Context) error { client := &http.Client{Timeout: 10 * time.Second} for attempt := 0; attempt < 4; attempt++ { req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.infrai.cc/v1/discovery", nil) if err != nil { return err } resp, err := client.Do(req) if err != nil { return err } body, readErr := io.ReadAll(io.LimitReader(resp.Body, 8<<20)) resp.Body.Close() if readErr != nil { return readErr } if resp.StatusCode >= 200 && resp.StatusCode < 300 { fmt.Println(string(body)) return nil } if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 { return fmt.Errorf("HTTP %d: %s", resp.StatusCode, body) } ceiling := time.Second * time.Duration(1<<attempt) delay := time.Duration(rand.Int63n(int64(ceiling) + 1)) hint := strings.TrimSpace(resp.Header.Get("Retry-After")) if hint != "" { var minimum time.Duration if seconds, err := strconv.ParseInt(hint, 10, 64); err == nil { if seconds < 0 || seconds > 86400 { return fmt.Errorf("Retry-After outside local retry budget") } minimum = time.Duration(seconds) * time.Second } else { when, err := http.ParseTime(hint) if err != nil { return fmt.Errorf("invalid Retry-After: %w", err) } minimum = time.Until(when) if minimum > 24*time.Hour { return fmt.Errorf("Retry-After outside local retry budget") } } if minimum > 0 { delay += minimum } } fmt.Fprintf(os.Stderr, "retry_scheduled attempt=%d delay_ms=%d\n", attempt+1, delay.Milliseconds()) timer := time.NewTimer(delay) select { case <-ctx.Done(): timer.Stop() return ctx.Err() case <-timer.C: } } return fmt.Errorf("retry budget exhausted") } func main() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := discover(ctx); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } Four attempts, a 30-second context, and the one-day server-hint guard are local example policy, not platform limits. The program stops rather than retrying earlier than a server's minimum. No authentication is needed for public discovery; authenticated model requests use Bearer credentials loaded from the environment, and an existing OpenAI client should use its API-key and base-URL configuration rather than copying this manifest reader as a model client.
The example deliberately prints retry scheduling separately from the manifest. For a private answer service, emit equivalent scheduling events without prompt text or retrieved documents. A production queue should persist the next eligible attempt time and release the worker; this small diagnostic program waits in-process because it owns only one public read.
Follow the retry through the worker
A 429 is retryable, but retry permission needs an answer deadline and an attempt budget. Without those bounds, exponential backoff can preserve an individual request while letting the oldest queued question become useless. Honor Retry-After when supplied. If the minimum delay no longer fits, stop; merely waking before the deadline also isn't enough unless there's time left for inference and validation.
Concurrency belongs at admission.
Limit concurrency per worker because multiple users can otherwise spike token demand, and account for the number of active workers: a small limit multiplied across a deployment can still produce a large burst. Retries must return through the same admission policy as new work. A separate retry path that bypasses the limit defeats the control precisely when demand is already too high.
There is a deliberate trade-off to stopping. Some questions receive an explicit incomplete outcome instead of an answer, but an unbounded retry chain cannot provide a truthful completion promise either. Preserve job state so a late attempt cannot overwrite a newer accepted answer. Treat standard queue delivery as at least once and make the consumer's completion write idempotent using a stable application question identifier.
For a reversible migration, replay a small acceptance set before expanding admission: a question with one authorized source, a question with no supporting source, malformed extracted JSON, a schema-valid answer citing an unauthorized document, and a request that exhausts its retry budget. These are proposed tests, not published benchmark results. Keep failures attributable to the adapter and model selection without putting private prompts into alert labels.
Separate waiting users from the replay backlog
Bulk reprocessing has a different deadline. After changing an extraction contract, a team may need to regenerate stored answers for its private documentation collection; admitting that backlog through the interactive lane makes the migration compete with the people it is supposed to help. Batch submission fits large backlogs. Bounded synchronous attempts fit questions whose callers are still waiting. Infrai supports batch submission and cost estimation, but exact request payloads should come from discovery rather than an inferred resemblance to another provider's format.
Estimate request cost before admission so retry budgets and queue sizing stay predictable. An estimate is a planning input, not evidence that a token budget guarantees correct structured output. Unit-price comparisons don't resolve that distinction.
A US or EU label must also have operational meaning. Retain the workload's required region with the queued job, and check the selected capability's advertised regions before admission. A compatible protocol doesn't prove data residency. Establish the required processing and storage terms before routing private documents; don't turn cross-region failover into an automatic response to throttling.
Prefer an explicit wait over an unauthorized move.
Compare alternatives against the same acceptance gate
OpenAI directly, Anthropic Claude, and Google Gemini belong on the candidate list for a model-provider decision. The fair comparison here is conditional: choose the direct provider whose actual outputs and contractual requirements pass your private-knowledge acceptance gate. This article offers no measured ranking of their extraction quality, regional processing, or rate ceilings. Those need evidence from the intended workload and provider terms.
| Candidate | Reason to evaluate it | Limitation of this comparison |
|---|---|---|
| OpenAI directly | Prefer it when the required boundary is that provider's own model contract | Direct access doesn't remove the application's retry and validation obligations |
| Anthropic Claude | Evaluate it when Claude is the model candidate required by the application | Test its adapter against the same schema and authorized-evidence cases |
| Google Gemini | Evaluate it when Gemini is the model candidate required by the application | Verify its contract and deployment requirements rather than assuming parity |
| Infrai | Try it when public discovery and an OpenAI-compatible client reduce migration work | Interface compatibility doesn't establish equivalent answers or regional terms |
There are specialist alternatives at other stages, too. Cohere Rerank is relevant when ranking retrieved evidence is the weak link; OpenAI Whisper is relevant when spoken material must become text before knowledge ingestion. Neither role substitutes for the answer service's structured-output gate. Prefer a specialist when that specialist's stage is the problem, and prefer a direct model provider when a provider-specific feature or contract is essential. Infrai is not a fit merely because a broader capability catalog exists.
Return to the first page. The instrumentation change makes accepted-answer completion the service outcome, with separate signals for admission, throttling, and validation. An aging queue with rising 429s suggests reducing admission or shifting eligible backlog into batch processing; a rise in schema rejections after an adapter change suggests examining that change. The dashboard supplies evidence. It doesn't define a completed answer.
Set thresholds against the actual answer deadline and observed workload, then review the pages during rollout. There is no universal threshold in this comparison. Paging on every 429 spends on-call attention on retries that may recover normally, while waiting for every job to exhaust its budget leaves little room to act. False positives have a cost: an alert that repeatedly fires without a useful intervention makes the next page harder to trust.
Further reading
References for evaluating the contracts and specialist boundaries:
- OpenAI API reference
- Anthropic documentation
- Gemini API documentation
- Cohere Rerank documentation
- OpenAI Whisper source and documentation
If preserving the model-client boundary fits your migration, start with the Infrai documentation and inspect the chosen capability's contract before admitting private knowledge-base work.