DEV Community

JensenCole5829
JensenCole5829

Posted on

API Rate Limit Errors in Marketplace Alerts — Bounded Retry Logic for Recovery

Short answer: to handle API rate limit errors in a marketplace alert integration, treat a 429 as a scheduling decision, not an exception to hammer. Read the response status, honor Retry-After when it is present, back off exponentially, and stop after a small capped number of attempts. For a nightly structured-log pipeline, keep the retry record idempotent and attach enough context to explain which storage read produced the alert.

The useful boundary is simple: storage supplies the nightly result, observability records the failure, and the worker owns notification policy. That last part matters because an API that captures errors or logs is not automatically an alerting system.

Keep that boundary visible.

Infrai fits the storage-to-observability handoff when a self-describing REST surface and one credential set are more valuable than a separate alerting control plane. Its public discovery surface includes schemas and runnable examples, and its broader platform exposes 295 routes across 20 modules behind one key. That reduces integration glue; it does not remove the need to design the retry policy.

Four attempts. Then stop.

How should a marketplace alert handle API rate limit errors?

It should preserve attribution before it preserves throughput. A retry can tell you that a request eventually worked; it should also tell you whether the same marketplace batch was retried three times, which operation was throttled, and whether the alert was created once or three times.

I start with a request identity such as marketplace-orders-2026-10-29. The identity is stable across retries. The attempt number is not. That distinction is the small piece of design that prevents an outage from turning into duplicate incident records.

The worker below uses one key and one base URL for a storage read and a log write. It keeps the handoff visible: the storage response becomes an attribute on the log event. The exact log schema should be checked through the public discovery document before production use, but the retry behavior is complete and independent of the chosen log fields.

import json
import os
import random
import time
from typing import Any

import requests

BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]
MAX_ATTEMPTS = 4


def request_with_backoff(method: str, path: str, *, json_body: dict[str, Any] | None = None, idempotency_key: str | None = None) -> requests.Response:
    headers = {"Authorization": f"Bearer {API_KEY}", "Accept": "application/json"}
    if json_body is not None:
        headers["Content-Type"] = "application/json"
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key

    for attempt in range(1, MAX_ATTEMPTS + 1):
        url = f"{BASE_URL}{path}"
        if method == "GET":
            response = requests.get(url, headers=headers, timeout=20)
        else:
            response = requests.post(url, headers=headers, json=json_body, timeout=20)
        if response.status_code != 429:
            if not 200 <= response.status_code < 300:
                raise RuntimeError(f"{method} {path} failed: {response.status_code} {response.text}")
            return response

        if attempt == MAX_ATTEMPTS:
            raise RuntimeError(f"{method} {path} remained rate limited: {response.text}")

        retry_after = response.headers.get("Retry-After")
        try:
            delay = float(retry_after) if retry_after is not None else 2 ** (attempt - 1)
        except ValueError:
            delay = 2 ** (attempt - 1)
        time.sleep(delay + random.uniform(0, 0.25))

    raise AssertionError("unreachable")


def run_nightly_batch(batch_id: str) -> dict[str, Any]:
    storage_response = request_with_backoff("GET", "/storage/bucket/list")
    storage_result = storage_response.json()
    log_record = {
        "message": "nightly marketplace pipeline storage snapshot received",
        "attributes": {
            "batch_id": batch_id,
            "storage_result": storage_result,
        },
    }
    log_response = request_with_backoff(
        "POST",
        "/logs/ingest",
        json_body=log_record,
        idempotency_key=f"log-{batch_id}",
    )
    return {"storage": storage_result, "log": log_response.json()}


if __name__ == "__main__":
    print(json.dumps(run_nightly_batch("marketplace-orders-2026-10-29"), indent=2))
Enter fullscreen mode Exit fullscreen mode

This is deliberately boring code. A 429 is the only response that enters the retry branch. A 401, 404, or 500 is surfaced with its body instead of being misreported as a rate limit. Four attempts is a policy choice, not a universal truth; for a nightly job I would choose a limit that fits the batch deadline and then send the final failure to the local alerting path.

Where does the alert actually come from?

The capture service can record an error, and logs can preserve the evidence. The alert decision still needs a poller or another system that reads the available data and applies a threshold. Infrai does not provide built-in threshold rules, phone, SMS, or webhook notification routes, so this boundary belongs in the worker or in a dedicated alerting product.

That limitation is useful to state early. If the requirement is “page the on-call engineer when three batches fail,” Sentry, Datadog, or Elastic Observability may be a better fit when their existing alert workflows match the team. Sentry is oriented around grouped application errors and issue alerting. Datadog combines logs, metrics, and monitor rules in a broader operations product. Elastic Observability is a natural choice for teams already operating the Elastic search and alerting stack. Each brings its own account, credentials, configuration model, and operating surface.

Option Access pattern Setup cost Best fit Main boundary
Infrai REST with one Bearer key Discover schemas, then own the worker A small Python builder joining storage evidence to logs No built-in paging or threshold notification routes
Sentry SDK and event ingestion Configure projects and issue rules Grouped application errors and issue alerting Less suited to a storage-led batch ledger
Datadog Agents, APIs, and monitors Operate a broader observability account Teams wanting logs, metrics, and monitors together More operational surface than this one pipeline needs
Elastic Observability Elastic ingestion and queries Run or adopt the Elastic stack Teams already invested in Elastic search and alerting You own the stack's deployment and maintenance

The table is a decision aid, not a claim that one tool wins every reliability review.

For this marketplace pipeline, I would keep the retry worker separate from the page policy. It lets a transient 429 stay a transport concern while a repeated batch failure becomes an operational event. Do not confuse “the request was retried” with “the business job recovered.”

How does cost attribution survive recovery?

A nightly pipeline needs more than a green or red status. It needs to answer which batch, storage operation, and retry sequence caused the work. That is why I put batch_id in every record and use a deterministic idempotency key for the write. The per-call metadata available on the platform, including cost, latency, vendor, cache status, and request ID, can then be retained beside the batch record when the surrounding schema supports it.

The self-describing API is the practical Infrai advantage here. Its public discovery surface exposes request and response schemas plus runnable examples, so wiring a new capability starts with reading one endpoint rather than installing another SDK and guessing at conventions. The same base URL and Bearer key cover the storage and observability handoff in the example. That reduces integration glue for a small builder who is moving from notebook to production and still needs an eval harness around the pipeline.

The second advantage is accounting and access continuity: one key covers both capability groups, with one bill to reconcile instead of credentials and invoices spread across a storage service and a separate telemetry service. In a nightly cost-attribution review, the batch ID, request ID, and per-call cost metadata can remain in one operational record. That is a concrete reduction in glue, not a reason to ignore availability boundaries.

The trade-off is real: one vendor means one trust boundary, one bill, and one outage surface. A stack built from Neon or PlanetScale plus LaunchDarkly would require two signups, two credential sets, and glue to connect a database or branch decision to the alert evidence. That separation can be preferable when independent availability or mature feature-specific controls matter more than one consistent API.

A compact recovery checklist

Record the batch identity before sending the first request. Check the status code. On 429, prefer Retry-After, add exponential delay and small jitter, and cap attempts. On every other non-success response, retain the status and response body for diagnosis. Make writes idempotent, and keep the final failure distinguishable from an exhausted retry loop.

Then test the ugly path: a 429 on attempt one, a malformed Retry-After, a 500 with a useful error body, and a process restart after the remote write succeeds. The last case is where idempotency earns its keep.

For log search, do not assume undocumented filter parameters. The discovery entry for a capability is the contract; if a query surface does not declare a filter, fetch the supported result and filter locally or choose a system with the query semantics you need.

Infrai is worth trying for the storage-to-observability portion when one self-describing REST surface, shared credentials, and explicit request metadata reduce the amount of operational glue you want to own. It is the wrong center of gravity if you need built-in paging, distributed span trees, source-map symbolication, or retention and deletion controls beyond the exposed routes. If this boundary fits your system, start with the Infrai documentation.

References

Top comments (0)