Cloudflare rate limiting works by counting requests that match an expression, over a period, against a threshold, and running an action once that threshold is crossed within the period. The expression decides which requests count, commonly a specific path like /api or /login. The threshold and period set the pace, for instance one hundred requests in sixty seconds. The action decides what happens next: block, challenge, or log only. Start every rule in log-only mode against real traffic before switching it to block, so the threshold you picked gets validated before it can lock out real users.
The mechanism is well documented. What is missing from most explanations is a starting number to actually type in, and how to roll a rule out without locking yourself, or a customer, out of a login page during a demo.
Table of contents
- The four pieces of a rule
- Sane starting thresholds
- Edge vs app-level rate limiting
- A minimal app-level limit as backup
- Testing without locking yourself out
- Handling a 429 on the app side
- Rolling a rule out safely
- How this fits the rest of the stack
- FAQ
The four pieces of a rule
The expression uses fields like the request URI path and method, and can combine conditions with “and” to scope a rule tightly. The counting key defaults to the visitor’s IP, but can be a header, a cookie, or a fingerprint for more precision when many users share an IP, a mobile carrier or a corporate network behind one address. The period is a rolling window measured in seconds. The action ranges from block, to a managed or JavaScript challenge, to logging only with no effect on the request.
A concrete rule for a login endpoint might look like: expression matches requests where the path equals /api/login and the method is POST, counted by IP address, threshold of five within a sixty-second period, action set to a managed challenge rather than an outright block. The challenge lets a real user prove they are human and continue, while a scripted brute-force attempt stalls at that step, which is usually the better default than a hard block on anything short of confirmed abuse.
Sane starting thresholds
There is no universal number, but there are reasonable starting points by endpoint type. A login endpoint tolerates far fewer attempts, somewhere around five to ten per minute per IP is a common starting point before tightening. A general API endpoint depends heavily on how many calls one legitimate page load makes, often a few hundred per minute is a safer starting ceiling than it sounds.
The honest answer is to pull the real number from your own traffic logs rather than guess, which is exactly what log-only mode gives you before any rule goes live. Watch a normal day, note the highest legitimate burst any single IP produces, usually a shared office connection or a mobile carrier’s NAT gateway, and set the threshold comfortably above that rather than at a round number picked without evidence.
A search or autocomplete endpoint deserves its own thinking entirely, since a single legitimate user typing normally can fire several requests a second without doing anything wrong. A threshold copied from a login endpoint would flag ordinary typing as abuse, which is exactly the kind of mismatch log-only mode is meant to catch before it reaches real users.
Edge vs app-level rate limiting
Edge limiting, done at Cloudflare, stops abusive traffic before it ever reaches your server, which is cheaper for you since a blocked request never costs compute. App-level limiting, in nginx or an Express middleware, is still necessary for logic the edge cannot see, per-account limits after authentication, for example, or if traffic is not routed through Cloudflare at all.
Running both layered is normal, not redundant: coarse IP-based protection at the edge catches the bulk of abusive traffic cheaply, while fine per-account limits in the app catch the case an IP-based rule structurally cannot, one authenticated user hammering an endpoint from a rotating pool of addresses. Neither layer alone covers both failure modes.
There is also a cost argument for putting the coarse rule at the edge specifically: a request blocked before it reaches your infrastructure never spins up a process, never opens a database connection, and never shows up in your own compute bill. An application-level rule still has to receive and parse the request before it can reject it, which is strictly more expensive per blocked request even though the end result looks the same to the attacker.
A minimal app-level limit as backup
http {
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
server {
location /login {
limit_req zone=login burst=3 nodelay;
proxy_pass http://app;
}
}
}
This nginx configuration limits the login path to roughly five requests per minute per client IP, with a small burst allowance. It runs regardless of whether the edge rule is in place, which is exactly the point of defense in depth: one layer failing or being bypassed does not leave the endpoint unprotected.
The equivalent in an Express application looks similar in intent: a middleware tracking request counts per IP in memory or in Redis, rejecting with a 429 once the window’s limit is exceeded. Either approach works; the choice usually comes down to whether nginx already sits in front of the app for other reasons, in which case it is the simpler place to add the rule, rather than introducing a second dependency purely for rate limiting.
Testing without locking yourself out
Set the rule to log-only first, always. Run a small script from your own IP to confirm the expression matches only the requests you intend, not the whole site by accident.
for i in $(seq 1 20); do
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/api/ping
done
If the rule is only meant for the public, consider exempting a known office or CI IP explicitly so testing and deploys are never affected. Dashboard access to Cloudflare itself is a separate system from a zone’s request-level rules, but a challenge placed on your own login page can still be genuinely annoying. Confirm that before it ships, not after a teammate gets locked out during a demo.
Keep a second, unaffected way into the application while a new rule is being validated, a VPN path, a second domain, or simply the option to disable the rule quickly from the dashboard. The five minutes spent confirming an escape hatch exists is cheap compared to discovering, mid-incident, that the only way back in is blocked by the very rule causing the incident.
Handling a 429 on the app side
A client that receives a 429 should read the Retry-After header if one is present and back off rather than retrying immediately, which just recreates the problem the rule exists to stop. On the server side, return a clear 429 with that header instead of a silent connection drop, and log which rule fired so a traffic spike is diagnosable later instead of starting investigation from zero.
The failure case worth avoiding on the client side is a naive retry loop that fires again the instant a request fails, without reading the response at all. That pattern turns a temporary rate limit into a sustained one, since the retries themselves keep hitting the threshold. An exponential backoff that respects Retry-After when present is a small amount of code that prevents a genuinely embarrassing self-inflicted outage, and it is worth adding to any client calling an endpoint you know is rate limited, not just the ones that have already been caught by it.
Rolling a rule out safely
- Deploy in log-only mode and let it run against real traffic for a few days
- Review the match counts and confirm the rule is catching the traffic you intended
- Tighten the expression if it is matching more broadly than expected
- Switch to a challenge before block for anything not obviously malicious
- Reserve block for clear abuse patterns, login brute forcing or scraping
Skipping the log-only step is the most common way a rate limiting rule causes its own incident: a threshold that looked reasonable on paper turns out to be below what a single legitimate integration partner sends in a normal minute, and the first anyone hears about it is a support ticket, not a dashboard alert. Treat the sequence above as the actual minimum, not an optional precaution for a rule you feel confident about, since confidence without traffic data is exactly how avoidable outages happen.
How this fits the rest of the stack
A rate limiting rule protects the app behind it, but the app still needs to survive the traffic that gets through, a login endpoint under a slow brute-force attempt, or a spike that stays under any single rule’s threshold. A service with autoscaling between a floor and ceiling plan absorbs a genuine spike without a human paging themselves at two in the morning, and runtime logs show exactly which requests made it past the edge rule and what they did next. Size a plan that can take a real spike on the RunxBuild hosting calculator. See Cloudflare’s rate limiting rules documentation for the exact expression syntax.
Useful related references:
- Nginx Rate Limit: limit_req, limit_conn, and Burst
- 429 Error Meaning: You Are Being Rate Limited, and What to Do About It
- Cloudflare Managed Ruleset: What the WAF Rules Actually Do, Which Plan Gets Which, and How to Stop the False Positives
- Services on RunxBuild
FAQ
Does Cloudflare rate limit on the free plan?
Cloudflare’s WAF rate limiting rules are available in a limited form on lower plans, with the number of rules and some advanced counting options reserved for higher tiers. Check the current plan comparison on Cloudflare’s own documentation before assuming a feature is included.
How does Cloudflare rate limiting actually work?
A rule defines an expression that matches certain requests, counts how many match within a period, and compares that count against a threshold. Once the threshold is crossed inside the period, the rule’s action runs, which can be blocking the request, issuing a challenge, or just logging it.
How many requests per second should I allow?
There is no universal number. A login endpoint tolerates far fewer attempts per minute than a general API endpoint. Start any new rule in log-only mode, watch your actual traffic for a few days, and set the threshold from what real legitimate use looks like rather than a guess.
How do I test a rate limit rule without locking myself out?
Deploy it in log-only mode first, run requests from your own IP to confirm the expression matches what you intend, and check the match log rather than the live behavior. Only switch to block or challenge once you have confirmed the rule targets the right traffic.
What should my app do when it gets rate limited?
Read the 429 response and the Retry-After header if one is present, then back off instead of retrying immediately. On the server side, return a clear 429 with that header rather than a silent connection drop, and log which rule fired so a spike is diagnosable later.