CMS API Rate Limiting Explained: Why It Matters and How to Handle It (2026)
The 429 response, Retry-After, and how clients should back off
You built a slick frontend on top of a headless CMS. Everything works in dev. Then you push to prod, traffic spikes, and half your page loads start failing with a cryptic 429. Welcome to rate limiting — the thing nobody thinks about until it breaks their site.
Rate limiting isn't the CMS being mean to you. It's the wall that keeps one noisy client from taking down the API for everyone else. If you're consuming a headless CMS over HTTP, understanding how limits work — and how to build a client that respects them — is the difference between a site that stays up and one that flakes under load.
TL;DR
Rate limiting caps how many API requests a client can make in a time window. CMS APIs need it to stop abuse, control server cost, and stay stable under bursty traffic. When you cross a limit, the server sends HTTP 429 Too Many Requests, usually with a Retry-After header telling you how long to wait.
Common algorithms: fixed window (simple, bursty at edges), sliding window (smoother), and token bucket (allows short bursts, refills over time). Most CMS APIs use per-route limits — reading posts gets a higher ceiling than logging in.
The fix on your side isn't hammering harder. It's caching responses, backing off when you get a 429, and building your pages statically or with ISR so you don't hit the API on every single visitor request. If you're picking a CMS, check the rate limits on its content API before you commit. UnfoldCMS, for example, sets public reads at 60/min and documents every route limit up front — more on that below.
What is API rate limiting, exactly?
Rate limiting is a rule the server enforces: "you get X requests per Y seconds, then I say no." It's tracked per client — usually by IP address, API token, or user ID. Cross the line and the server rejects further requests until the window resets. It's a throttle, not a ban.
The point is fairness and survival. An API has finite CPU, memory, and database connections. Without a cap, one badly written loop or one scraper can eat all of it, and every other client suffers. Rate limiting draws a line so no single caller can starve the rest.
Why do CMS APIs need rate limiting at all?
Three reasons: abuse, cost, and stability. A public content API is a scraping target — bots will pull your whole catalog if you let them. Every request costs database queries and CPU, which costs money. And bursty traffic (a launch, a viral post) can knock an unprotected API over. Limits keep it predictable.
Auth endpoints are the sharpest case. A login route with no limit is an open invitation for credential-stuffing — bots trying thousands of password combos per minute. That's why login and register almost always get much tighter caps than read routes. If you're wiring up auth against a headless CMS, the token and auth flow matters as much as the limit sitting in front of it.
What are the common rate-limiting algorithms?
Three show up most often. Fixed window counts requests in fixed time blocks (e.g. per calendar minute) — simple, but allows double bursts at window edges. Sliding window tracks a rolling time range, smoothing those bursts. Token bucket hands you tokens that refill at a steady rate; you spend one per request and can burst until the bucket's empty.
Here's how they compare:
| Algorithm | How it works | Burst handling | Complexity |
|---|---|---|---|
| Fixed window | Count resets every fixed interval | Poor — 2x burst at window boundary | Low |
| Sliding window | Rolling time range, weighted count | Good — smooths edge bursts | Medium |
| Token bucket | Tokens refill over time, one per request | Great — allows controlled bursts | Medium |
| Leaky bucket | Requests drain at a fixed rate | Smooths output, queues excess | Medium |
Most frameworks (Laravel, Express middleware, API gateways) default to a fixed or sliding window because it's cheap and predictable. Token bucket shows up when the API wants to allow short spikes without punishing normal use.
What happens when you hit a CMS API rate limit?
The server stops processing your request and returns HTTP 429 Too Many Requests. Well-behaved APIs include a Retry-After header — either seconds to wait or an HTTP date — plus rate-limit headers showing your ceiling, remaining requests, and reset time. Your job is to read those headers and wait, not retry instantly.
The response body usually carries a short error message too. Here's a typical 429 from a JSON API:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 30
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1751558400
{
"success": false,
"message": "Too Many Attempts."
}
X-RateLimit-Remaining: 0 means you're out. Retry-After: 30 means wait 30 seconds. Don't guess — read these values.
How should a client handle rate limits?
Respect the headers, back off, and stop making requests you don't need. When you get a 429, wait the Retry-After duration before retrying. For repeated failures, use exponential backoff — wait longer each attempt. Cache anything that doesn't change often. Best of all, build pages statically so visitors never trigger a live API call.
Here's the practical checklist:
- Read the rate-limit headers on every response — track
X-RateLimit-Remainingso you can slow down before you hit zero, not after. - Honor
Retry-Afteron a 429 — wait exactly that long. Retrying immediately just extends the block. - Use exponential backoff with jitter — on repeated failures, wait 1s, 2s, 4s, 8s, plus a small random offset so many clients don't retry in sync.
- Cache aggressively — content that changes hourly doesn't need to be fetched per request. A short-lived cache (CDN, Redis, or in-memory) kills most of your traffic.
- Build static or use ISR — with Next.js, Astro, or similar, fetch content at build time or revalidate on an interval. Your visitors hit static HTML, not the CMS API. This is the single biggest lever.
- Batch and paginate sensibly — pull 50 posts in one request, not 50 single-post requests. Fewer calls, fewer limit hits.
- Use a server-side token, not a per-visitor call — proxy CMS requests through your backend so all traffic shares one authenticated, cacheable path.
That fifth point deserves emphasis. If you're rendering a blog from a headless CMS and every reader triggers a fresh API fetch, you'll hit limits the moment you get real traffic. Static generation or incremental static regeneration (ISR) means you fetch once, serve thousands. The REST vs GraphQL tradeoffs matter here too — how you query affects how many calls you make.
What do real CMS API rate limits look like?
Per-route limits are the norm — read routes get generous ceilings, auth and write routes get tight ones. A concrete example: UnfoldCMS ships a REST API at /api/v1/* with different caps per route type. Public reads are the loosest; login and register are the strictest, since those are the abuse-prone ones.
Here are the actual UnfoldCMS per-route limits:
| Route type | Limit | Why this number |
|---|---|---|
| Public read (posts, pages, search) | 60 / min | Generous for real frontends, blocks scrapers |
Authenticated user (/me) |
120 / min | Logged-in dashboards make more calls |
| Admin write (create/update content) | 30 / min | Writes are heavier; lower ceiling protects the DB |
| Login | 5 / min | Tight — stops credential-stuffing |
| Register | 3 / min | Tightest — stops bulk fake-account creation |
Auth uses Sanctum tokens, and every response comes back in a consistent envelope — { "success": true, "data": {...} } on success, { "success": false, "message": "..." } on error — so your client can check one field to know if a call worked. That predictability makes handling a 429 easier: same shape whether it's a hit or a miss. You can see the full REST API surface on the features page.
How to build a client that respects these limits
Wrap your CMS calls in a small fetch helper that checks the status and headers before returning data. On a 429, read Retry-After, wait, and retry — with a cap on attempts so you fail loud instead of looping forever.
async function fetchFromCms(url, opts = {}, attempt = 0) {
const res = await fetch(url, opts);
if (res.status === 429 && attempt < 4) {
const retryAfter = Number(res.headers.get('Retry-After')) || 2 ** attempt;
await new Promise(r => setTimeout(r, retryAfter * 1000));
return fetchFromCms(url, opts, attempt + 1);
}
if (!res.ok) throw new Error(`CMS API ${res.status}`);
return res.json();
}
Then don't call this on every page render. Call it at build time, cache the result, and revalidate on a schedule. With a 60/min read limit, a static build that fetches once per deploy will never come close to the ceiling — even with millions of visitors. The pattern scales because your traffic and your API traffic are decoupled.
If you do need live data (search, personalized feeds), proxy it through your own backend with a short cache. One shared server-side token, one cache layer, and most of your would-be API calls disappear before they ever reach the CMS. For the broader picture on consuming these APIs, the CMS with REST API guide walks through the full setup.
FAQ
Is a 429 the same as being blocked or banned?
No. A 429 is temporary. It clears when your rate-limit window resets — often in seconds. A ban is a deliberate, longer block (usually a 403). If you honor Retry-After, a 429 just means "slow down," not "go away."
What's the difference between 429 and 503?
429 means you sent too many requests — it's about your rate. 503 Service Unavailable means the server is overloaded or down for everyone. Both may include Retry-After, but only 429 is specific to your request rate.
Can I raise a CMS API rate limit? Sometimes. Self-hosted CMS platforms let you edit the throttle config directly. Hosted/SaaS CMS APIs usually offer higher tiers. But raising the limit is rarely the right first move — caching and static builds usually eliminate the need entirely.
Should I retry a 429 immediately?
Never. Immediate retries add load and often extend your block. Wait the Retry-After duration, or use exponential backoff if the header's missing. Add a little random jitter so many clients don't all retry at the same instant.
Do rate limits count per IP or per token? Depends on the route. Public unauthenticated reads are usually limited per IP. Authenticated routes limit per token or user. That's why a shared server-side token can concentrate all your traffic under one predictable limit instead of scattering it.
Ship a client that doesn't fight the API
Rate limits aren't an obstacle to route around — they're a signal to design around. Cache what you can, build static where possible, back off when told to, and read the headers. Do that and you'll rarely see a 429 in the first place.
If you want a headless CMS that documents its limits openly and returns a consistent response envelope so your error handling stays simple, take a look at what UnfoldCMS offers. Knowing the numbers up front — 60/min reads, 5/min login, and the rest — means you can design your fetch layer once and stop worrying about it.
Sources: rate-limiting algorithm behavior and the HTTP 429 / Retry-After semantics follow the IETF HTTP specifications (RFC 9110, RFC 6585). UnfoldCMS route limits reflect the shipped /api/v1/* throttle configuration as of 2026.
Free & Open Source
Own your CMS. No subscriptions.
Unfold CMS is free to download and self-host. Built on Laravel + React, full source code included.
Share this post: