Cloudflare Rate Limiting: 13 Steps, 60 Min [2026]

Cloudflare rate limiting is the WAF control that stops one client from hammering a login form, a search box or an API route. On October 21, 2026, Cloudflare made full transaction detection generally available and said custom rules and managed rules now ship on every plan, Free included. Rate limiting rules live in the same WAF, and they are usually where a small team sees its first real abuse.

Free zones also get their own Cloudflare Free managed ruleset, according to a December 2025 small-business security write-up from dchost, so the vendor-maintained detections now cover almost everyone. Rate limiting is the part you write yourself. This tutorial builds it in 13 steps over roughly 60 minutes: Log mode first, Block second, and the whole setup as Terraform code at the end. If your site is not behind Cloudflare yet, start with our Cloudflare WAF setup walkthrough and come back.

You will finish with a working lab: a tiny origin server, a rate limiting ruleset deployed by Terraform, a Python probe that proves the limit trips, and a rollback path. Every rule starts in Log mode so you can watch real traffic before anyone gets a 429.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

What Cloudflare rate limiting does in 2026

A rate limiting rule counts requests that match an expression, groups them by a key such as the client IP, and takes an action once a group crosses a threshold. Cloudflare documents two enforcement behaviors: block requests for a set duration after the threshold is reached, or throttle only the requests that exceed the configured rate. The first suits login forms. The second suits public APIs where you want steady clients to keep working.

The product name matters because old tutorials mislead. Cloudflare’s current documentation calls the feature Rate Limiting Rules, built on the Ruleset Engine, and keeps the earlier standalone feature on legacy pages marked as the previous version. New automation must use the Rulesets API and the cloudflare_ruleset Terraform resource. If a guide tells you to call a rate_limit endpoint or use a cloudflare_rate_limit resource, close the tab.

Two recent changes shape how you use the feature. First, the October 21 release moved the WAF beyond request-only filtering. Customers on every plan can create up to three response custom rules with Log or Block actions, which inspect what the origin sends back. Second, Cloudflare keeps shipping dated managed-ruleset releases (October 14 emergency, October 28, October 30 and November 3), so your hand-written throttles now sit beside a ruleset that changes weekly. That is a good reason to keep your own rules in version control instead of clicking them into the dashboard.

Where rate limiting sits in the WAF pipeline

Order of execution decides whether your rule ever sees a request. Cloudflare documents three relevant phases, and custom rules run before rate limiting, which runs before managed rules. A request blocked by a custom rule never reaches your throttle. A request that your throttle blocks never reaches the managed rulesets. Keep that in mind each time a counter looks lower than expected.

OrderFeaturePhase nameWhat it does
1Custom Ruleshttp_request_firewall_customYour own expressions with Block, Managed Challenge, Skip or Log
2Rate Limiting Ruleshttp_ratelimitCounts matching requests and acts above a threshold
3Cloudflare Managed Rulesethttp_request_firewall_managedVendor-maintained detections for known vulnerabilities
3OWASP Core Rulesethttp_request_firewall_managedCloudflare’s managed implementation of the OWASP CRS

The OWASP Core Ruleset is a separate product from the Cloudflare Managed Ruleset, and treating them as one causes confusion in support threads. The upstream OWASP CRS project publishes its own releases, and its newest mainline release at the time of writing is v4.30.0 from October 2, 2026, alongside the v4.25.2 long-term support line. Cloudflare’s offering is a managed implementation of that rule family, not a copy you install yourself.

If you are weighing Cloudflare against other vendors before committing, our Cloudflare WAF vs AWS WAF vs Imperva comparison covers the trade-offs. For a wider view of the field, the cybersecurity section lists the tools we have tested.

Prerequisites and versions

You need a Cloudflare account with at least one zone whose DNS record is proxied (orange cloud). A grey-cloud record bypasses the WAF entirely, and that is the single most common reason a rule appears broken. The lab also assumes a Linux or macOS shell. Windows users can run everything inside WSL.

RequirementVersion or settingWhy
Cloudflare zoneAny plan, DNS record proxiedRate limiting rules apply only to proxied traffic
API tokenZone WAF Edit and Zone Read, scoped to one zoneLeast privilege for the Rulesets API
Terraform1.5 or newerPlan and import behavior used in this lab
Cloudflare Terraform provider4.x line (block syntax)The 5.x line uses a different attribute syntax
Python3.10 or newer, with requestsProbe script
curl and jqcurl 7.68+, jq 1.6+API calls and JSON inspection
OriginAny HTTP server, or the 25-line lab origin belowSomething to rate limit

No public server? Run the lab origin on your laptop and publish it through a tunnel. Our Cloudflare Tunnel setup guide shows how to get a proxied hostname pointing at a local port in about ten minutes, and it keeps the origin off the public internet, which is what you want when the whole point is abuse control.

Plan limits to check before you design rules

Plan limits shape the design more than any syntax does. Cloudflare’s rate limiting documentation lists one rule on Free, 2 on Pro, 5 on Business and 100 on Enterprise. Treat those as a snapshot and read the live page before you plan, since limits move and the docs describe them per plan. On a Free zone you will likely get one rule, so the lab ships with the login throttle first and an optional second rule behind a Terraform flag.

Lower plans also restrict which counting characteristics, periods and mitigation timeouts validate. The documented API periods are 10, 60, 120, 300, 600 and 3,600 seconds, with the available values varying by plan. The shortest period is the safest bet for a first deployment on any plan, so the project defaults to 10 seconds. Complexity-based counting is limited to Enterprise customers with Advanced Rate Limiting, and account-level rate limiting rulesets that deploy to many zones need Enterprise as well.

One more planning point. A 10-second window catches bursts, such as a script firing 30 login attempts in a second, but it will not catch a slow attacker who sends one attempt every 15 seconds. Longer windows need a plan that allows them. Pair the throttle with a challenge or a lockout inside your own application and treat Cloudflare as the first line, not the only one.

Part 1: Prepare the account, token and test origin (Steps 1 to 3)

Step 1: Create a scoped API token (5 min)

In the Cloudflare dashboard, open your profile, go to API Tokens and create a custom token. Grant two permissions on a single zone: Zone WAF with Edit access, and Zone with Read access. Do not use the Global API Key. A token scoped to one zone limits the damage if the file leaks into a repository, and Terraform and curl both accept it as a bearer credential.

Export it together with the zone ID, which appears on the zone’s overview page. Keep these in your shell session or a password manager, never in a committed file.

export CLOUDFLARE_API_TOKEN="paste-token-here"
export ZONE_ID="paste-zone-id-here"
export HOSTNAME_UNDER_TEST="lab.example.com"

curl -sS "https://api.cloudflare.com/client/v4/user/tokens/verify" \
  -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" | jq '.result.status'

Expected output is the single word active. Anything else means the token is expired, mistyped or disabled, so fix that before moving on.

Step 2: Start the lab origin (5 min)

Create a project folder named cf-ratelimit-lab and save the file below as origin.py. It answers GET requests with 200 and every POST with 401, which mimics a login endpoint that rejects bad credentials. That detail matters later because you will see 401 turn into 429 once the throttle kicks in.

#!/usr/bin/env python3
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer


class Handler(BaseHTTPRequestHandler):
    def _reply(self, code, body):
        data = json.dumps(body).encode()
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)

    def do_GET(self):
        self._reply(200, {"path": self.path, "ok": True})

    def do_POST(self):
        length = int(self.headers.get("Content-Length", 0))
        self.rfile.read(length)
        self._reply(401, {"error": "bad credentials"})

    def log_message(self, fmt, *args):
        print(self.client_address[0], self.command, self.path)


if __name__ == "__main__":
    ThreadingHTTPServer(("127.0.0.1", 8080), Handler).serve_forever()

Run it with python3 origin.py and confirm it answers locally with curl -i http://127.0.0.1:8080/api/ping. Then publish port 8080 through your tunnel or point your proxied DNS record at the host that runs it.

Step 3: Take a baseline through Cloudflare (5 min)

Before you add any rule, prove that traffic flows through the proxy and that nothing limits you yet. Fire 15 login attempts and count the status codes. Every response should be 401 from your origin, with a cf-ray header proving Cloudflare handled the request.

for i in $(seq 1 15); do
  curl -s -o /dev/null -w "%{http_code}\n" -X POST "https://${HOSTNAME_UNDER_TEST}/login"
done | sort | uniq -c

curl -sI "https://${HOSTNAME_UNDER_TEST}/api/ping" | grep -i -E "^(HTTP|server|cf-ray)"

You should see 15 401 from the first command and a server: cloudflare line from the second. If the server header is missing, the record is not proxied. Fix the DNS record now, or nothing later in this tutorial will work.

Part 2: Build the rules in Log mode (Steps 4 to 7)

Step 4: Decide what to protect and at what rate (10 min)

Write the policy on paper before you touch a config. Pick the endpoints where one client sending many requests costs you money or security: login, password reset, signup, search and any expensive API call. Then choose a threshold from your own logs. The table below shows starting points for the lab. They are opinions to adjust, not Cloudflare defaults.

EndpointMatchStarting thresholdFinal actionReasoning
LoginPOST to /login5 per 10 secondsBlockHumans rarely retry more than a few times
Password resetPOST to /reset3 per 10 secondsManaged ChallengeStops email-bombing without locking out real users
Public APIPaths under /api/30 per 10 secondsBlockGenerous for apps, tight for scrapers
SearchGET to /search10 per 10 secondsManaged ChallengeExpensive queries deserve friction first

Cloudflare’s own Security Week product discussion on Cloudflare TV uses a threshold of 10 requests over 2 minutes as its teaching example, and walks through the rule builder with request fields such as headers and user agent as optional narrowing criteria. That session is worth ten minutes if you have never seen the builder. Your numbers should come from your traffic, not from anyone’s slide.

Step 5: Pick an action for each rule (5 min)

Cloudflare documents the actions Block, Managed Challenge, Skip and Log for custom rules, and rate limiting rules apply logging, blocking or challenges after a threshold is exceeded. Choose per endpoint, using the table below as a guide.

ActionWhat the client seesBest used for
LogNothing, the request passes throughFirst deployment and threshold tuning
Managed ChallengeA challenge page that real browsers usually passForms that humans use and bots abuse
BlockAn error response, typically HTTP 429Machine-only endpoints and confirmed abuse
SkipLater rules are skipped (custom rules only)Allowlisting health checks or partner IPs

Every rule in this lab starts with Log. The cost of a wrong Block is a locked-out customer, while the cost of a wrong Log is an extra row in your events view. Because the action sits in one Terraform variable, flipping the whole ruleset later is a one-line change.

Step 6: Write the match expression (10 min)

Expressions use Cloudflare’s rules language, the same one custom rules use. Cloudflare says custom rules can match on request information such as IP address, URL path, headers and request body. For rate limiting you usually need the host, method and path. Two functions save you from the most common mistakes: lower() makes path comparison case-insensitive, and starts_with() catches trailing slashes and file extensions that an exact match would miss.

# Login: only POSTs, any trailing path
(http.host eq "lab.example.com" and http.request.method eq "POST" and starts_with(lower(http.request.uri.path), "/login"))

# API: all routes under /api/, but never verified bots such as search crawlers
(http.host eq "lab.example.com" and starts_with(lower(http.request.uri.path), "/api/") and not cf.client.bot)

The not cf.client.bot clause keeps Cloudflare’s verified bots out of your counter, which stops you from throttling a crawler that you want indexing your docs. Add exemptions deliberately. Each one is a hole an attacker can aim at.

Step 7: Choose the counting key and window (5 min)

The counting key, called characteristics in the API, decides who shares a bucket. The default and safest choice is the client IP, written ip.src. On lower plans the API expects cf.colo.id alongside it, so the lab uses ["cf.colo.id", "ip.src"]. That pair means the count is kept per Cloudflare data center per IP, so a client spread across several data centers gets a separate bucket in each. For most login abuse that is fine because one machine usually hits one location.

Cloudflare’s security architecture documentation also describes counters based on request or header criteria, sessions and API tokens. Those matter when many users share one IP, as they do behind a school or office NAT. Counting by an API key header keeps one noisy customer from punishing everyone else on the same address.

Part 3: Deploy, test, tune and enforce (Steps 8 to 11)

Step 8: Deploy the rules with the Rulesets API (8 min)

Start with raw API calls so you see what Terraform will later automate. A rate limiting ruleset is the entry point of the http_ratelimit phase, and you write it with a single PUT. Warning: PUT replaces every rule in that phase, so on a zone that already has throttles you must fetch the current rules first and include them in your payload.

#!/usr/bin/env bash
# api-put.sh
set -euo pipefail
: "${CLOUDFLARE_API_TOKEN:?}" "${ZONE_ID:?}" "${HOSTNAME_UNDER_TEST:?}"

# Inspect what already exists before overwriting it
curl -sS "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets/phases/http_ratelimit/entrypoint" \
  -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" | jq '.result.rules // []'

curl -sS -X PUT "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets/phases/http_ratelimit/entrypoint" \
  -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data @- <<JSON | jq '.success, .result.rules[0].id'
{
  "rules": [
    {
      "action": "log",
      "description": "login-post",
      "expression": "(http.host eq \"${HOSTNAME_UNDER_TEST}\" and http.request.method eq \"POST\" and starts_with(lower(http.request.uri.path), \"/login\"))",
      "ratelimit": {
        "characteristics": ["cf.colo.id", "ip.src"],
        "period": 10,
        "requests_per_period": 5,
        "mitigation_timeout": 10
      }
    }
  ]
}
JSON

A successful call prints true followed by the new rule’s ID. If it prints false, read the errors array in the raw response. The usual causes are covered in the troubleshooting section below.

Step 9: Run the probe and read the events (10 min)

Now generate traffic that should cross the threshold. The probe below sends a configurable number of requests, prints each status code and summarizes the run. Save it as probe.py and install the dependency with pip install requests.

#!/usr/bin/env python3
import argparse
import collections
import time

import requests


def main():
    ap = argparse.ArgumentParser(description="Probe a rate limited URL")
    ap.add_argument("url")
    ap.add_argument("--method", default="GET")
    ap.add_argument("--count", type=int, default=20)
    ap.add_argument("--delay", type=float, default=0.2)
    args = ap.parse_args()

    codes = collections.Counter()
    first_limited = None
    session = requests.Session()
    body = {"user": "probe", "password": "wrong"} if args.method == "POST" else None

    for i in range(1, args.count + 1):
        resp = session.request(args.method, args.url, data=body, timeout=10, allow_redirects=False)
        codes[resp.status_code] += 1
        if resp.status_code == 429 and first_limited is None:
            first_limited = i
        retry = resp.headers.get("Retry-After", "-")
        ray = resp.headers.get("cf-ray", "-")
        print(f"{i:>3}  {resp.status_code}  retry-after={retry}  ray={ray}")
        time.sleep(args.delay)

    print("---")
    for code, n in sorted(codes.items()):
        print(f"HTTP {code}: {n}")
    print(f"first 429 at request: {first_limited or 'never'}")


if __name__ == "__main__":
    main()
python3 probe.py "https://lab.example.com/login" --method POST --count 20 --delay 0.2

With the rule in Log mode, the probe still sees 401 on every request, because Log never interferes. The proof lives in Cloudflare’s Security events view for the zone. Filter by the rule description login-post and you should see logged events appear once more than five requests land inside a 10-second window. A typical probe summary looks like this (the exact ray IDs will differ):

  1  401  retry-after=-  ray=8f3a...-AMS
  2  401  retry-after=-  ray=8f3a...-AMS
  ...
 20  401  retry-after=-  ray=8f3a...-AMS
---
HTTP 401: 20
first 429 at request: never

The word never in that last line is correct for Log mode. If events do not show up, jump to the troubleshooting list. Do not move on until you can see at least one logged event, since a rule you cannot observe is a rule you cannot trust.

Step 10: Tune against real traffic (15 min of waiting and reading)

Leave the rules in Log mode for at least a full business day on a live site. Then open the events and sort by client IP. You are hunting for false positives: office networks, mobile carriers that share addresses, uptime monitors and integration partners. Each legitimate source that trips the rule needs either a higher threshold, a different counting key or an explicit exemption.

Credential stuffing is the attack this rule exists to slow, and a throttle only slows it. If your logs show a login flood from many IPs at low individual rates, the per-IP limit will not catch it, and a challenge on the form is the better fit. Our Cloudflare Turnstile setup guide covers adding that challenge without a visible puzzle. If a flood already succeeded, check whether the stolen passwords are circulating with the steps in our dark web monitoring guide.

Step 11: Switch to Block or Managed Challenge (5 min)

Once the logs look clean, change the action. Do it for one rule first, run the probe from your own machine, and confirm that you see the 429 pattern. The summary should now show some 401 responses followed by a run of 429s, and the first 429 should land around request six.

  1  401  retry-after=-   ray=8f3b...-AMS
  ...
  5  401  retry-after=-   ray=8f3b...-AMS
  6  429  retry-after=10  ray=8f3b...-AMS
  7  429  retry-after=10  ray=8f3b...-AMS
 ...
---
HTTP 401: 5
HTTP 429: 15
first 429 at request: 6

Treat those numbers as the expected shape, not a guarantee. Counting is distributed and the window slides, so your first 429 may land at request four or eight. If it lands at all, the rule works. If it never lands, go back to the troubleshooting list. Remember to wait out the mitigation timeout before you test again, or your next run will start already blocked.

Part 4: Ship it as code and keep watching (Steps 12 and 13)

Step 12: Move the ruleset into Terraform (8 min)

Clicking rules into a dashboard works until two people do it on the same afternoon. Terraform gives you review, history and rollback. The resource is cloudflare_ruleset, the phase is http_ratelimit, and the same rule fields from the API map across directly. If a rule already exists in that phase from Step 8, import it first, or Terraform will complain that a ruleset already exists.

# main.tf
terraform {
  required_version = ">= 1.5.0"
  required_providers {
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 4.0"
    }
  }
}

# The provider reads CLOUDFLARE_API_TOKEN from the environment
provider "cloudflare" {}

# variables.tf
variable "zone_id" {
  type = string
}

variable "hostname" {
  type = string
}

variable "rl_action" {
  type    = string
  default = "log"
}

variable "rl_period" {
  type    = number
  default = 10
}

variable "rl_timeout" {
  type    = number
  default = 10
}

variable "rl_login_requests" {
  type    = number
  default = 5
}

variable "rl_api_requests" {
  type    = number
  default = 30
}

variable "enable_api_rule" {
  type    = bool
  default = false
}

# ratelimit.tf
locals {
  login_expr = "(http.host eq \"${var.hostname}\" and http.request.method eq \"POST\" and starts_with(lower(http.request.uri.path), \"/login\"))"
  api_expr   = "(http.host eq \"${var.hostname}\" and starts_with(lower(http.request.uri.path), \"/api/\") and not cf.client.bot)"

  rules = concat(
    [{ name = "login-post", expr = local.login_expr, requests = var.rl_login_requests }],
    var.enable_api_rule ? [{ name = "api-routes", expr = local.api_expr, requests = var.rl_api_requests }] : []
  )
}

resource "cloudflare_ruleset" "rate_limit" {
  zone_id = var.zone_id
  name    = "rate-limit-lab"
  kind    = "zone"
  phase   = "http_ratelimit"

  dynamic "rules" {
    for_each = local.rules
    content {
      description = rules.value.name
      enabled     = true
      action      = var.rl_action
      expression  = rules.value.expr

      ratelimit {
        characteristics     = ["cf.colo.id", "ip.src"]
        period              = var.rl_period
        requests_per_period = rules.value.requests
        mitigation_timeout  = var.rl_timeout
      }
    }
  }
}

# outputs.tf
output "ruleset_id" {
  value = cloudflare_ruleset.rate_limit.id
}

In a real repository, split that block into the four files its comments name. Pin the provider to the 4.x line so the block syntax above validates. The 5.x line replaces nested blocks with attribute lists, so port it deliberately and run terraform validate after every change. Then deploy:

terraform init
terraform plan  -var "zone_id=${ZONE_ID}" -var "hostname=${HOSTNAME_UNDER_TEST}"
terraform apply -var "zone_id=${ZONE_ID}" -var "hostname=${HOSTNAME_UNDER_TEST}"

# Flip to enforcement later with one variable
terraform apply -var "zone_id=${ZONE_ID}" -var "hostname=${HOSTNAME_UNDER_TEST}" -var "rl_action=block"

The plan should show one resource to add, and apply prints the ruleset ID as an output. Commit the files, but never commit terraform.tfstate or any file containing your token. Use a remote backend with locking once a second person joins.

Step 13: Set up monitoring and a rollback plan (10 min)

A throttle that you never look at drifts into either uselessness or an outage. Put a recurring reminder on the calendar to review rule events weekly for the first month. Watch three numbers: how many events each rule fires, how many distinct IPs trigger it, and whether support tickets mention being blocked or challenged. A sudden jump in distinct IPs means a distributed attack, which a per-IP rule cannot stop alone.

Write the rollback as a command, not a hope. Setting rl_action=log and applying takes under a minute and leaves the rules in place for analysis. If you need to stop everything at once, terraform destroy -target=cloudflare_ruleset.rate_limit removes the ruleset. Test both paths once while nothing is on fire. Teams that skip this step discover during an incident that nobody remembers the zone ID.

The complete working project at a glance

Here is the finished layout. Each file appears in full above, so you can copy the project together in about five minutes. The Terraform block from Step 12 splits into the four files shown.

cf-ratelimit-lab/
  origin.py              # Step 2: local test origin, GET 200, POST 401
  probe.py               # Step 9: request generator and summary
  api-put.sh             # Step 8: raw Rulesets API deployment
  main.tf                # Step 12: provider and version pin
  variables.tf           # Step 12: action, period, thresholds, flags
  ratelimit.tf           # Step 12: cloudflare_ruleset in http_ratelimit
  outputs.tf             # Step 12: ruleset ID
  terraform.tfvars       # zone_id and hostname (do not commit)
  .gitignore             # *.tfstate*, terraform.tfvars, .terraform/

The workflow runs in one loop. Edit a threshold in the variables file, run terraform plan, apply it in Log mode, run the probe, read the events, and only then switch the action. That loop takes about five minutes once it is wired up, and it makes a threshold change a reviewed pull request instead of a Friday-evening dashboard edit.

Pre-enforcement checklist: what to measure before you flip to Block

Switching from Log to Block is the riskiest moment in this whole project, so give it a gate. Run through the list below and write the answers in the pull request that changes rl_action. Reviewers should be able to approve the change without opening the dashboard.

  • Observation window. The rule ran in Log mode through at least one full business cycle, including the busiest hour of your week. A rule that only saw a quiet Sunday has told you nothing.
  • Top triggering IPs. You reviewed the ten noisiest sources and classified each as attacker, shared network, monitor or partner. Anything in the last three groups has an exemption or a higher threshold.
  • Distinct IP count. If hundreds of different addresses each trip the rule once, you are looking at a normal traffic spike or a distributed attack, and a per-IP limit is the wrong tool for either.
  • Error budget. You know how many legitimate users the rule could affect. Zero is the target, and a few affected users need a named owner who accepts the risk.
  • Support path. Your help desk knows that a 429 or a challenge page can come from the edge and not the application, and they know how to ask a customer for the ray ID.
  • Rollback tested. You ran the Log-mode apply and the targeted destroy from Step 13 at least once, and you timed them.

Teams often ask how long the observation window should last. There is no universal answer, but a useful rule of thumb is to wait until you have seen your most unusual normal day: the payroll run, the product launch email or the monthly report export. Those events produce the bursts that look most like abuse. If the rule survives them in Log mode without flagging real users, enforcement is much safer.

Also decide what success looks like before you enforce. For a login throttle, a good result is a drop in failed-login volume from the noisiest sources with no rise in password-reset tickets. For an API throttle, watch origin CPU and the share of requests from the top five clients. If a number does not move after a week, the rule is either too loose or aimed at the wrong endpoint, and you should say so in the review instead of leaving a dormant control in place.

Common pitfalls that break rate limiting rules

  1. Overwriting existing rules with PUT. The entry point call replaces the whole phase. Always read the current rules first and merge, or manage everything through one Terraform resource so there is a single owner.
  2. Going straight to Block. A threshold guessed from a blog post will hit a real user within a week. Log first, read a full business cycle of events, then enforce.
  3. Forgetting the evaluation order. Custom rules run before rate limiting, so a custom rule that blocks or skips traffic changes what your counter sees. A Skip rule written for a partner IP can quietly exempt that IP from your throttle too.
  4. Counting the wrong thing. Counting per IP punishes shared networks, and counting per data center lets a distributed client multiply its allowance. Check who actually shares your IPs before you pick the key.
  5. Matching the path exactly. An expression that equals /login misses /login/, /Login and /login.php. Use lower() and starts_with().
  6. Throttling health checks and webhooks. Payment providers retry on failure, and your own uptime monitor hits one URL every few seconds. Exempt them by IP or header before enforcement, not after the first outage.
  7. Copying an Enterprise example onto a Free zone. Periods, timeouts and counting characteristics that validate on one plan fail on another. Start from the shortest period and the IP-based key.

Troubleshooting: 10 problems and their fixes

  1. API returns error 10000 or an authentication failure. The token lacks Zone WAF Edit, targets a different zone, or has expired. Re-run the verify call from Step 1, then check the token’s zone scope.
  2. Request is rejected because the period or timeout is not allowed. Your plan does not support that value. Drop the period to 10 seconds, set the mitigation timeout to 10 and use the IP-based key shown in this tutorial.
  3. Rule never fires and the probe shows no events. Confirm the DNS record is proxied and that the probe’s cf-ray header is present. A request that never touched Cloudflare cannot be counted.
  4. Rule fires in the dashboard but the probe never sees 429. You are still in Log mode. Log records the event and lets traffic through by design.
  5. Terraform says a ruleset already exists for the phase. The API call from Step 8 created one. Import it with the zone ID and ruleset ID, or delete the entry point rules and re-apply.
  6. A custom rule or Skip rule seems to override your throttle. Check the order table above. Anything that blocks or skips in the custom phase decides the request before rate limiting runs.
  7. Everyone behind one office gets blocked. They share an IP. Raise the threshold, count by a session cookie or API key header where your plan allows it, or exempt the office range.
  8. Static files inflate the count. The expression is too broad. Narrow it to the paths that matter, and consider counting only requests that reach the origin if your plan exposes that option.
  9. Search crawlers drop out of the index. Your API or search rule is hitting verified bots. Add not cf.client.bot to the expression and re-check the events.
  10. Old tutorials reference endpoints and resources that fail. They describe the previous rate limiting product. Use the Rulesets API and cloudflare_ruleset, as shown in Steps 8 and 12.

If none of these fits, run the same request with curl -v and read the response headers. A Cloudflare-branded error page or a cf-ray header tells you which layer answered. Support threads also move faster when you paste the ray ID.

Advanced tips for production traffic

Pair throttles with response rules. Since October 21, every plan can create up to three response custom rules with Log or Block actions. A login endpoint that returns 401 or 403 for failures is the textbook case: a response rule can flag the failed responses themselves rather than every request. Treat it as a complement to rate limiting, not a replacement, and test it in Log first just like the throttles.

Use detection fields in your own expressions. Cloudflare’s November 3 changelog reportedly lists a cf.appsec.request.failed_detections field that you can use in custom rules and rate limiting rules. Which detections you can see depends on your plan, so verify availability in your dashboard before building on it. If it works for you, it lets a throttle count only requests that already tripped a detection.

Plan for Enterprise features only if you need them. Account-level rate limiting rulesets, complexity-based counting and Advanced Rate Limiting are Enterprise territory. Teams with one or two zones rarely need them. If you run dozens of zones, a shared account-level ruleset replaces copy-pasted Terraform. Our Cloudflare WAF vs AWS WAF vs Azure WAF pricing comparison and the Cloudflare vs CloudFront test help you judge whether those tiers pay off.

Watch the managed rulesets too. Cloudflare ships dated releases, including a November 3 update to PHP out-of-memory detections for CVE-2026-109618, CVE-2026-109619 and CVE-2026-115560. Its changelog also reportedly schedules a November 10 change that adds a dangerous file upload detection, initially set to Log. Some new detections default to Log and some to Block. Paid-plan customers were told to verify the Managed Ruleset is deployed in Block mode after the October 21 PHP protections. Rate limiting reduces volume, but it is no substitute for those checks.

Ask for rules, but review them. Cloudflare has announced a partnership with OpenAI Daybreak models, and its press release says customers can instruct Cloudflare to deploy custom WAF rules for specific attack vectors. Whatever generates a rule, a human should read the expression, run it in Log and put it in version control like everything else.

Add a CI guardrail. A short check that fails the pipeline when rl_action is anything other than log on a branch that is not main prevents the classic accident of testing a Block rule on production from a feature branch. Pair it with terraform fmt -check and terraform validate.

Where rate limiting stops and other layers start

Rate limiting answers one question: how fast is this client sending requests. It does not tell you whether the client is a person, whether a password is stolen or whether the request body carries an exploit. Layer the controls. Managed rulesets handle known exploit patterns, challenges separate humans from scripts, and your application should still lock accounts after repeated failures and log every attempt.

Keep the origin firewall tight as well. Cloudflare can only help if traffic goes through it, so allow only Cloudflare ranges or a tunnel at the network edge. If you run your own gateway, our OPNsense firewall setup and pfSense setup guides cover the rules that close the back door. Finally, document who owns each threshold and when it was last reviewed. A rule without an owner turns into the thing everyone is afraid to touch.

FAQ: Cloudflare rate limiting

Is Cloudflare rate limiting free?

Cloudflare documents Rate Limiting Rules as available across plans, with the number of rules and the allowed periods and timeouts depending on the plan. Its docs list one rule on Free. Check the live documentation and your dashboard for current limits, since the sources we reviewed did not publish current dollar prices for paid tiers.

What is the difference between custom rules and rate limiting rules?

Custom rules decide per request using an expression and run in the http_request_firewall_custom phase. Rate limiting rules add a counter, so they act only after a client sends too many matching requests, and they run in the http_ratelimit phase after custom rules.

Should I start with Block or Log?

Start with Log. It records what would have been limited and lets every request through, so you can find false positives such as shared office IPs before a customer finds them for you. Switch to Block or Managed Challenge after a full business cycle of clean events.

Which Terraform resource should I use?

Use cloudflare_ruleset with the phase set to http_ratelimit. Cloudflare says new automation must use the Rulesets API and that resource. The old cloudflare_rate_limit resource belongs to the previous version of the product.

Why do I get a 429 from my own IP?

Your test run crossed the threshold and the mitigation timeout has not ended. Wait for it to expire, then lower the request count in your probe or raise the threshold. Also check that no other device on your network shares the same public IP and sends matching requests.

Does rate limiting stop credential stuffing?

It slows attacks that come from few IPs at high speed. Attacks spread across thousands of IPs at low rates slip under a per-IP threshold, so add a challenge on the login form, application-level lockouts and breach monitoring.

Do full transaction detection and response rules replace rate limiting?

No. Full transaction detection, generally available since October 21, 2026, correlates requests with responses and powers up to three response custom rules per customer. Rate limiting still handles volume per client, and the two controls complement each other.

Elias Virtanen

Elias Virtanen

Cybersecurity Analyst

Elias Virtanen is the Cybersecurity Analyst at Tech Insider, bringing hands-on expertise from his background in penetration testing and security consulting. He previously worked as a security researcher at F-Secure in Helsinki, where he focused on threat intelligence and vulnerability disclosure. Elias covers ransomware trends, zero-trust architecture, and the evolving regulatory landscape including NIS2 and the EU Cyber Resilience Act. He holds a CISSP certification and an MSc in Information Security from Aalto University.

View all articles