API Rate Limiting and Broken Integrations
There's a particular flavor of integration bug that drives site owners crazy: the sync that works perfectly in every test, works fine most days, and then fails — always partially, often at the same time of day, never with a useful error message in the UI. The orders stopped flowing to the fulfillment service Tuesday morning. The email list sync skipped 300 contacts. The inventory update quietly gave up two-thirds of the way through.
Nine times out of ten, nothing is "broken." Your integration hit a rate limit: the API on the other end said you're asking too fast, come back later, and your integration — or the plugin wrapping it — didn't handle that answer gracefully. Rate limiting isn't an error condition; it's a normal, documented, guaranteed-to-happen part of how every serious API operates. Integrations that treat it as an exotic failure break. Integrations that treat it as weather — routine, expected, planned for — don't.
Why APIs limit you (it's not personal)
Every API is a shared resource. Behind Stripe's or Mailchimp's or Google's endpoints sit finite servers, and one badly written client in a retry loop can generate more traffic than ten thousand well-behaved ones. Rate limits exist to:
- Protect the service from accidental self-inflicted DDoS — the classic being a cron job gone feral, retrying a failing request as fast as the CPU allows.
- Guarantee fairness — your burst of 10,000 requests shouldn't add latency for every other customer.
- Enforce the business model — free tiers get 100 requests a minute, paid tiers get 10,000. The limit is the product boundary.
- Blunt abuse — credential stuffing and scraping get much harder at 60 requests per minute.
The practical takeaway: the limit is a contract term, not an obstacle. The API is telling you, precisely, how fast it's willing to work with you. Everything that follows is about reading and honoring that contract.
How limits actually work: the three common models
Fixed window
"100 requests per minute" counted against clock minutes: your counter resets at :00 of each minute. Simple to implement and to reason about, with one quirk — you can legally send 100 requests at 11:59:59 and 100 more at 12:00:01, a 200-request burst in two seconds. Some APIs are fine with that; some layer a burst limit on top.
Sliding window
"100 requests per any rolling 60-second period." Smoother and stricter: there is no reset moment to exploit. If you sent 100 requests in the last 60 seconds, whenever those seconds were, you wait.
Token bucket
The most common model among big APIs, and the most useful to understand. Imagine a bucket that holds, say, 60 tokens. Every request spends one token. The bucket refills at a steady drip — say, one token per second. The consequence: you can burst (spend 60 tokens in five seconds if the bucket is full) but you can't sustain more than the refill rate (one request per second, on average, forever). This matches real workloads beautifully — quiet most of the time, occasionally bursty — and explains a behavior that confuses people: why your first batch of calls flies and then everything abruptly slows. You drained the bucket; now you're living on the drip.
Many APIs also apply different limits per endpoint (reads cheap, writes expensive, search very expensive) and some use cost-based quotas where a complex query costs more "points" than a simple one. Always check the docs for the specific endpoints you hammer.
Reading the signals: 429 and the headers that matter
When you exceed a limit, a well-behaved API responds with HTTP status 429 Too Many Requests. That response usually carries the most valuable headers in this whole subject:
| Header | Meaning |
|---|---|
Retry-After | Seconds to wait (or an exact date) before trying again. If present, this is the answer — obey it, don't guess. |
RateLimit-Limit / X-RateLimit-Limit | Your ceiling for the current window |
RateLimit-Remaining / X-RateLimit-Remaining | Requests you have left right now |
RateLimit-Reset / X-RateLimit-Reset | When the window resets (timestamp or seconds) |
Names vary across providers (a standardized RateLimit-* convention exists but adoption is gradual), yet the trio of limit/remaining/reset is near-universal. A mature integration reads Remaining on every response and slows down before hitting zero — the difference between watching your fuel gauge and driving until the engine dies.
One warning: some APIs return 403 or even 503 for rate limiting instead of 429, and some return 429 with no headers at all. When the docs are vague, assume the strictest interpretation.
Handling limits properly: backoff, jitter, and idempotency
Here's the core algorithm every integration should implement, and the reason each piece exists.
1. Retry with exponential backoff
On a 429 (or a transient 5xx), don't retry immediately — you'll spend your remaining quota re-failing. Wait, then retry; if it fails again, wait roughly twice as long: 1s, 2s, 4s, 8s, 16s, then give up and alert. Exponential growth is the point: it makes a persistent problem cost you a handful of attempts instead of thousands, while still recovering from a blip in a second or two. If Retry-After is present, it overrides your schedule — the server knows better than your formula.
2. Add jitter
If your system retries after exactly 4 seconds, every request that failed together retries together — a synchronized stampede that hits the limit again in lockstep, fails together, and stampedes again. This is the "thundering herd," and the fix is embarrassingly simple: randomize each wait, e.g. sleep a random duration between 0 and the computed backoff. Jitter spreads the herd into a trickle. Every production-grade client library does this; if you're writing your own retry loop, so should you.
3. Make retries safe with idempotency
The scariest rate-limit scenario isn't a failed request — it's an ambiguous one. You sent "create order," got a timeout, retried… did the first one actually go through? Are there two orders now? The answer is idempotency keys: a unique ID you attach to each logical operation (many APIs, Stripe most famously, support an Idempotency-Key header). Retrying with the same key means the server executes the operation once no matter how many times you send it. If the API you use supports idempotency keys and your integration retries anything that isn't a pure read, use them. If it doesn't support them, retry writes only when you can verify state first.
4. Throttle yourself before the API does
The politest integration never sees a 429 because it self-limits below the documented ceiling — running comfortably below the allowed rate. Queue outbound requests and release them on a schedule instead of firing them as fast as loops allow. For bulk jobs (syncing 10,000 products), this turns "blast and pray" into a steady, predictable stream that finishes reliably, if a bit later.
5. Ask for less
The cheapest request is the one you don't send. Batch endpoints (many APIs accept 100 records per call — that's a 100× reduction), delta syncs (fetch what changed since the last run, not everything), caching responses that don't change often, and webhooks instead of polling (let the service tell you when something happens, rather than asking every minute — a topic that deserves its own post) all reduce pressure on the limit more effectively than any retry logic.
The WordPress angle: limits you don't see
On a WordPress site, most API traffic flows through plugins, which adds a layer of opacity worth understanding:
- Plugins share your quota. The API key your email marketing plugin uses has one rate limit — and if two plugins, a Zapier zap, and a custom function all use the same key, they're draining one bucket while blaming each other. Where possible, give each integration its own key or app registration; at minimum, know who shares what.
- Cron pileups cause the 9 a.m. mystery. WP-Cron fires scheduled tasks in clumps, and site-traffic-triggered cron makes timing worse. Several sync jobs landing in the same minute is exactly how a normally-fine integration exceeds a per-minute limit only at certain times. Spreading schedules (one sync at :00, another at :20, another at :40) is a two-minute fix for a chronic failure.
- Check the logs, not the UI. Plugin UIs love to report rate-limit failures as generic "sync failed." The plugin's log file (or a logging plugin capturing outbound HTTP) will show the 429 plainly. "Failed every Tuesday around 9" plus a 429 in the log is a diagnosis, not a mystery.
- Your own site rate limits inbound too. If you expose the WordPress REST API to external services, your host or security plugin may throttle them — remember that when a partner says your API keeps rejecting their calls.
A checklist for integrations that don't break
- Read the API's rate-limit documentation for the specific endpoints you use — numbers, headers, and per-endpoint quirks.
- Handle 429 explicitly: obey
Retry-After, otherwise exponential backoff with jitter, capped at ~5 attempts, then alert a human. - Watch
X-RateLimit-Remainingand slow down before zero. - Use idempotency keys on anything that creates or modifies data.
- Prefer batch endpoints, delta syncs, caching, and webhooks over brute-force polling.
- Stagger scheduled jobs so they don't share a minute.
- Log every 429 with a timestamp — the pattern is the diagnosis.
Rate limits are one of the few parts of a distributed system that come with a written promise: this is exactly how much you may ask of us, and this is exactly how we'll tell you when you've asked too much. Integrations break when that promise is ignored; they become boring and reliable when it's read. Boring and reliable is the goal. Save the excitement for your marketing.
Photo: Miguel Á. Padriñán on Pexels