March 19, 2025

How to Build a Rate-Limit Safe Client: Budgeting Requests Using Response Headers

featured image

APIs don’t usually fail because the endpoint is down. They fail because your client gets greedy.

You add one more dashboard, one more cron job, one more retry loop… and suddenly you hit the wall. Rate limits. Requests rejected. Data gaps. Angry alerts.

The fix isn’t “send fewer requests.” That’s vague and usually unrealistic.

A rate-limit safe client is built like a budgeting app. It reads the usage headers, predicts remaining capacity, and makes decisions before the API starts rejecting calls. And it still works even when headers are missing.

Here’s the implementation playbook.

In CoinAPI’s Market Data REST API, authenticated endpoints may include usage headers. Prioritize these when present:

  • X-Credits-Used — credits consumed in the rolling 24 hours
  • X-Credits-Remaining — credits remaining in the rolling 24 hours
  • X-RateLimit-Request-Cost — the cost of this response (when provided)

Legacy/alternate names (may still appear on some endpoints):

  • X-RateLimit-Remaining
  • X-RateLimit-Limit

Concurrency (when present; may vary by product/plan):

  • X-ConcurrencyLimit-Limit
  • X-ConcurrencyLimit-Remaining

Two details matter a lot:

  1. The request limit is a rolling 24-hour window, not “midnight reset.”
  2. Some headers (like Remaining/Used) may not be appended to every response.

That “may not always be appended” line is why you must design for missing headers from day one.

Treat headers like telemetry. Every call should capture:

  • timestamp (client time)
  • endpoint name (or route pattern)
  • HTTP status
  • the full set of credits/rate limit/concurrency headers (when present)

Store them in your logging/metrics pipeline. This is not “nice to have.” It’s how you debug throttling issues later.

What you want is a state object per API key like:

  • remaining
  • limit
  • used
  • reset_time
  • last_seen_at
  • request_cost
  • quota_remaining (if present)
  • concurrency_remaining (if present)

A clean mental model:

Budget = Remaining capacity over time
Your job is to not spend it faster than it refills.

With a rolling window, you can’t compute “refill per second” perfectly unless the API gives more info. But you can do something practical:

  1. If you have X-Credits-Remaining and a reset timestamp (when provided):
    • compute time until reset: T = reset_time - now
    • compute safe average rate: avg = remaining / T
    • apply a safety margin (e.g., use 80% of avg)
  2. If you do not have a reset time, but you do have limit+used via legacy headers:
    • remaining ≈ limit - used
    • use a conservative fixed pacing (see fallback section)
  3. If you have neither:
    • enter “blind mode” and use your own throttling rules + exponential backoff on 429/403.

This gives you a predictable pacing number even with partial info.

This is where teams get burned.

CoinAPI defines “request” as amount of data you ask for, not always “number of HTTP calls.”

If an endpoint supports a limit parameter, then every 100 output items counts as 1 request.

Meaning:

  • one HTTP call returning 1000 items could cost ~10 “requests”
  • your dashboard might look cheap in call-count but expensive in quota

So your budgeting math should use:

effective_spend = request_cost
from X-RateLimit-Request-Cost

If cost header isn’t present, assume cost = 1 (conservative, but safe).

When remaining capacity drops, your client should degrade gracefully.

A simple rule set that works:

  • Remaining > 20%
    Normal operation
  • 20% ≥ Remaining > 5%
    Start pacing:
    • increase delay between requests
    • prioritize critical endpoints
    • batch requests where possible
  • Remaining ≤ 5%
    Survival mode:
    • stop non-critical background jobs
    • serve cached data where acceptable
    • avoid large limit calls
    • reduce fan-out (don’t query many symbols per cycle)

Also watch subscription-wide headers:

  • If X-RateLimit-Quota-Remaining is low, you may be fine per key but still break the subscription pool.

That’s why storing both per-key and quota headers matters.

CoinAPI notes that some usage headers are not always appended.

So you need a rule:

Implementation idea:

  • Maintain last_known_remaining, last_known_reset, last_seen_at
  • If you haven’t seen headers for (say) 5 minutes:
    • assume remaining is lower than last known (don’t assume it replenished)
    • reduce request rate gradually
    • rely more on server responses (429 / Retry-After where applicable)

This avoids the classic mistake: “No headers means unlimited.”

When headers are missing:

  • use fixed max requests per second (small)
  • use jittered exponential backoff on errors
  • slowly ramp up only after a streak of successful responses

This mirrors how good TCP congestion control behaves.

Rate limits aren’t only “how many per day.” They’re also “how many at once.”

When present, cap in-flight calls based on:

  • X-ConcurrencyLimit-Limit
  • X-ConcurrencyLimit-Remaining

Practical behavior:

  • If concurrency_remaining is low:
    • stop launching new requests
    • wait for in-flight calls to finish
    • reduce parallelism dynamically

This prevents bursts that blow up during peak load.

Retries are where clients accidentally DDoS APIs.

Rules that keep you safe:

  • Never retry immediately on 429 / “limit exceeded”
  • Use exponential backoff with jitter
  • Respect server hints (Retry-After for WS headers exist; REST often uses reset time)
  • On repeated throttling, reduce concurrency and request rate globally (not per-call)

Also: don’t retry expensive “high limit” calls in a loop. Switch to smaller pages.

A simple control loop:

  1. Send request
  2. Read headers
  3. Update internal budget state
  4. Decide next delay and concurrency cap
  5. If budget low → degrade features
  6. If headers missing → fallback pacing
  7. On 429 → enter backoff mode and lower baseline

This is enough to run stable for months without manual babysitting.

  • Treating request count as the only cost (ignoring X-RateLimit-Request-Cost)
  • Assuming limits reset at midnight (they’re rolling 24h)
  • Not handling missing headers (your client becomes blind and overeager)
  • Retrying too aggressively (creates spikes and disconnects)
  • Using high parallelism without checking concurrency headers

Beyond reading headers in real time, CoinAPI also provides a Spend management feature in the Customer Portal.

This allows you to define usage limits and control whether overage is allowed at the subscription level. If overage is disabled and the subscription reaches its quota, further requests will be rejected until the rolling window resets.

This adds a second safety layer on top of your client-side budgeting logic, helping teams prevent unexpected charges and enforce internal usage policies across environments like production, staging, or public-facing applications.

✅ Reads and stores usage headers from every response
✅ Budgets based on Credits-Remaining + reset timestamp when available
✅ Uses Request-Cost as the unit of spend
✅ Degrades gracefully near low remaining capacity
✅ Survives missing headers using cached state + conservative pacing
✅ Caps concurrency based on concurrency headers (when present)
✅ Retries with jittered backoff, never in a tight loop

A rate-limit safe client is not an optimization. It’s part of your architecture.

Read the headers. Budget your capacity. Handle missing telemetry. Add spend controls at the subscription level.

If you're building on CoinAPI, use both layers:

  • client-side budgeting logic
  • subscription-level spend management in the Customer Portal

That combination gives you predictable usage, stable integrations, and no surprise rejections.

Start by instrumenting your headers today — before your first 429 forces you to.

background

Stay up-to-date with the latest CoinApi News.

By subscribing to our newsletter, you accept our website terms and privacy policy.

Recent Articles