Integrating third-party APIs with .NET and rate limits

Most third-party API integrations start with a straightforward HTTP call. Production adds quotas, token expiry, network faults, and traffic spikes. A request that succeeds from a developer laptop in Melbourne can earn a 429 Too Many Requests when the same feature runs across several Azure App Service instances. The goal is therefore to build a client that fails safely, recovers predictably, and makes provider limits visible.

A reliable design starts with the provider’s contract and ends with feedback from production. Between those points sit typed HTTP clients, bounded retries, throttling, cancellation, structured telemetry, and tests that reproduce limit behaviour. These concerns matter in Australia, where retailers in Sydney and Melbourne often connect storefronts to accounting systems such as Xero or MYOB while their customers check order status on phones during a train commute.

Define the provider contract first

Read the provider documentation before designing the abstraction. Record authentication scopes, endpoint quotas, pagination rules, idempotency support, error shapes, and the exact headers that expose remaining calls or a reset time. Limits may apply per API key, IP address, customer, tenant, or endpoint, so “requests per minute” alone is rarely enough. Keep a dated link to the policy and assign an owner who will check it when the provider changes.

If the payload contains personal information, assess obligations under Australia’s Privacy Act 1988 and the Australian Privacy Principles before adding logging. Tokenise identifiers where possible, restrict access to secrets, and avoid placing personal data or bearer tokens in traces. When provider data supports an Australian consumer purchase, the Australian Consumer Law can also affect how prices, refunds, and delivery promises are represented. Legal review should follow the actual data flow rather than rely on a generic compliance checklist.

Keep provider DTOs and JSON attributes at the integration edge. Map them into application models so a renamed response property cannot ripple through an ASP.NET Core service. Give the adapter a narrow interface, such as ICustomerDirectory or IShipmentGateway, and pass a CancellationToken into every network call. This structure makes contract changes easier to review and gives tests a seam where they can simulate 429 responses without calling a live service.

Build a reusable client

Register typed clients with IHttpClientFactory and dependency injection rather than constructing HttpClient inside each request. Set a base address, sensible timeout, and descriptive User-Agent; attach authentication through a DelegatingHandler or authorisation policy. A factory-managed client reuses connections safely, while each operation still creates a fresh HttpRequestMessage. Avoid global mutable headers because a token or correlation value can leak into another tenant’s call.

services.AddHttpClient<IShipmentGateway, ShipmentGateway>(client =>
{
    client.BaseAddress = new Uri(configuration["Shipping:BaseUrl"]!);
    client.Timeout = TimeSpan.FromSeconds(10);
});

For resilience, add the standard resilience handler from Microsoft.Extensions.Http.Resilience, or compose Polly policies explicitly. Keep timeouts shorter than the caller’s cancellation budget so a provider stall does not consume the whole request. If the service calls several dependencies, use separate named or typed clients; sharing one pipeline can let a slow provider alter the failure behaviour of unrelated calls.

Deserialize with System.Text.Json into explicit records, reject missing required values, and validate business constraints after parsing. Handle pagination instead of assuming one page contains every customer. In an Angular front end, my Angular EventEmitter guide shows how one component can share a backend response without triggering duplicate requests. That pattern complements a server-side integration that applies rate limiting once, at the boundary where it belongs.

Decode rate-limit signals

A 429 response is a signal to reduce demand, not an automatic invitation to hammer the endpoint. Read Retry-After when present: it may contain delay seconds or an HTTP date. Some APIs expose X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset; newer-style RateLimit-* headers may carry similar data. Build a provider-specific parser because header names, meanings, rounding, and reset units vary.

Turn limit information into a typed exception such as RateLimitExceededException, retaining the status code, retry delay, remaining quota, and request identifier. Do not log an unbounded response body, and never log credentials. A request ID helps support trace the incident to the provider’s logs. If a provider signals exhausted quota through a 403 response, treat that as a distinct configuration or entitlement problem unless its documentation explicitly defines retry semantics.

Use telemetry to compare RateLimit-Remaining over time rather than reading it only after failure. Record the number of rejected calls, observed delay, and endpoint label without high-cardinality customer values. When the server asks for a delay longer than an interactive request can wait, place the work on a durable queue or reschedule it rather than holding an ASP.NET Core thread. The user can receive a clear pending status while a background worker respects the provider’s window.

Control retries and outbound concurrency

Retry only failures likely to be transient: connection resets, selected 5xx responses, and 429s. Use exponential backoff with jitter, for example a base delay that doubles up to a provider-approved ceiling. Jitter prevents many workers from retrying on the same schedule after a shared outage. Respect a valid Retry-After value as a minimum; if it extends beyond the operation’s deadline, defer the job instead of retrying early. For creates, use the provider’s idempotency key or store a stable operation identifier so a network ambiguity does not duplicate an order.

Proactive throttling prevents the first burst. The System.Threading.RateLimiting package provides token-bucket and concurrency limiters that can sit inside a DelegatingHandler or service boundary. A token bucket controls the average request rate, while a concurrency limiter caps simultaneous in-flight calls. Configure these from the provider’s published quota and leave headroom for interactive traffic and background jobs. Also cap queued work because an unlimited queue converts a provider slowdown into memory pressure and an opaque backlog.

Remember that an in-process limiter is per application instance. Ten pods can collectively send ten times the intended traffic, so a larger deployment may need shared coordination through Redis, a gateway quota, or a central scheduler. Choose one owner for each outbound request and avoid a retry handler plus a queue worker both attempting the same operation. For predictable bulk work, a .NET BackgroundService with a bounded channel often gives cleaner pacing than web requests that happen to trigger jobs.

Production checklist for reliable integrations

Before deployment, use a request-focused checklist:

After deployment, pair those checks with operational safeguards:

These patterns should reflect local operating conditions. Australian online shoppers commonly expect mobile order tracking and clear delivery estimates, while businesses may synchronise Shopify or commerce data with Xero, MYOB, and payment services. Keep timestamps in UTC with DateTimeOffset, then render Australian local time and handle daylight-saving transitions correctly; preserve AUD amounts, GST treatment, and ABN details when the accounting system owns those records. For adjacent ideas on component and UI boundaries, Oscar’s writing is a useful companion when deciding how an Angular client should present delayed or partial provider data.

Ship with observability and a safe rollout

Start in a sandbox with recorded fixtures, then move through a limited canary before enabling the integration for every request. Contract tests can use a tool such as WireMock to replay normal pages, quota errors, and expired credentials. Load tests should stop at safe thresholds and never use a production token merely to see how far a shared quota can stretch. A feature flag gives operators a quick route to disable outbound traffic without redeploying the whole application.

Use OpenTelemetry traces and metrics to follow a logical operation across the ASP.NET Core service, queue, and provider. Include provider request IDs and calculated retry timing, but keep customer identifiers out of metric labels. Define alerts around sustained 429s, falling remaining quota, queue growth, and circuit-breaker state. A short runbook should name the provider contact, the quota owner, the kill switch, and the steps for replaying safely after an incident.

Pick one integration and make its failure path the first deliverable. Document its quota, add typed request handling, implement bounded backoff, and place one simulated 429 scenario in an automated test. Build that path today, then observe it in a canary before increasing traffic.