These are the Cloudflare free plan WAF rules running on my zone today: three custom rules and one rate limiting rule. I got there by reading a week of firewall events, and by undoing one change that blocked my own images.
The short version: path rules do most of the work. The rate limit is where you hurt yourself.
What 7 days of Cloudflare security events looked like
Between September 17 and 24 the zone saw about 34,000 requests. 55% of them hit a security action. The events sample I pulled had 5,208 rows from 920 IPs.
None of it was aimed at me. It was the same background noise every public hostname gets: .env files, .git/config, WordPress logins, a React RCE probe for CVE-2025-55182, empty user agents, Go-http-client.
The part that surprised me was cost. Blocked requests never reach the Worker. My busiest Worker peaked at 786 requests in a day, against a free allowance of 100,000.
To see your own, the GraphQL Analytics API has a firewall events dataset. On Free it's sampled and the hourly query budget is small, so don't loop it.
{
viewer {
zones(filter: { zoneTag: "<zone id>" }) {
firewallEventsAdaptive(
limit: 100
filter: { datetime_geq: "2026-09-17T00:00:00Z" }
orderBy: [datetime_DESC]
) { action source clientIP clientRequestPath userAgent clientASNDescription }
}
}
}
Is it an attack or background scanning?
Background scanning, in my case. Look at three things: the paths (secret files and CMS logins you don't have), the source network (cloud and VPS ranges, not home ISPs), and the timing (dozens of paths from one IP inside a second). If all three match, it's an automated scanner, and blocking it at the edge is enough.
One warning before you trust a report. The first analysis I got, written by an AI assistant from the same API data, listed /estimate as an unknown endpoint probe. That's my calculator page. It also counted /zh-Hant/ and /sitemap.xml. Check every "attack path" against your own routes.
Custom rules 1 and 2: block WordPress and secret-file paths
Free gives you 5 custom rules and no regex, so every rule is a chain of contains. I use two for paths.
The WordPress one. I don't run WordPress anywhere, so nothing legitimate matches:
(http.request.uri.path contains "/wp-admin") or (http.request.uri.path contains "/wp-includes")
or (http.request.uri.path contains "/wp-content") or (http.request.uri.path contains "/wp-json")
or (http.request.uri.path contains "/xmlrpc.php") or (http.request.uri.path contains "/wp-login")
or (http.request.uri.path contains "/wlwmanifest.xml") or (http.request.uri.path contains "/wordpress/")
or (starts_with(http.request.uri.path, "/wp/"))
The secret-file one is longer. The core of it:
(http.request.uri.path contains "/.env") or (http.request.uri.path contains "/.git/")
or (http.request.uri.path contains "/.aws/") or (http.request.uri.path contains "/.docker/")
or (http.request.uri.path contains "/credentials.yml") or (http.request.uri.path contains "/phpinfo")
or (http.request.uri.path contains "/info.php") or (http.request.uri.path contains "/dump.sql")
or (http.request.uri.path contains "/_profiler")
contains "/.env" already catches .env.production and /api/.env. You don't need a line per variant.
Why bother, if those files don't exist? A path rule blocks the first request. A rate limit only kicks in after the threshold, so a slow scanner walks under it forever.
Custom rule 3: block tool user agents, and pay for it
The third rule blocks two known IPs plus any user agent containing curl, Go-http-client, python, scrapy or fasthttp.
It works. It also bit me. My own ops notes said Python and Go clients were allowed. The live rule blocked both, and a bare curl https://guushu.com/ returns 403 today. When I check my own site now, I send a browser user agent.
Before copying this rule, list what calls your zone: webhooks, uptime checks, your own scripts. If any of them use a default library user agent, they're gone. This is the rule most likely to cause a quiet outage.
The one free rate limiting rule, and how it 429'd my own images
Free gets one rate limiting rule, counted per IP, with a fixed 10 second window and a fixed 10 second block.
The AI report suggested dropping mine from 20 to 10 requests per 10 seconds. I did. Three days later an image-heavy page on another subdomain was returning 429 on its own first screen. AI search crawlers were getting rate limited too: GPTBot, ClaudeBot, PerplexityBot.
The fix was to stop counting things that aren't worth counting:
((http.host eq "guushu.com") or ends_with(http.host, ".guushu.com"))
and not (http.request.uri.path.extension in {"js" "css" "png" "jpg" "jpeg" "gif" "webp" "avif" "svg" "ico" "woff" "woff2" "ttf" "json" "map" "txt" "xml" "webmanifest"})
and not cf.client.bot
Back at 20 requests. The test: 40 concurrent HTML requests from one IP gave 20 × 200 and 20 × 429. 40 concurrent static requests gave 40 × 200. [cf.client.bot](https://developers.cloudflare.com/ruleset-engine/rules-language/fields/reference/cf.client.bot/) is true only for Cloudflare's verified bots.
Does a 429 on Googlebot mean you're blocking Google?
Not necessarily. My logs had 107 rate limit events with a Googlebot user agent. Every one came from Google Cloud customer ranges, not the crawler. Google publishes its crawler IP ranges; check the source IP before you loosen anything. Real Googlebot and Bingbot had zero blocks.
Zone settings I changed, and ones I left off
On the settings side I moved minimum TLS to 1.2, turned on HSTS, and left Bot Fight Mode, AI Labyrinth and the Free managed ruleset on. The full list with a curl for each is in my Cloudflare zone security checklist.
One caution on HSTS. My header now says includeSubDomains; preload. Preload is hard to take back, so be sure every subdomain serves HTTPS before you add it.
I left three things off. Under Attack Mode puts a challenge in front of every visitor, which is a lot of friction for background noise. mTLS wants a client certificate that none of my visitors have. Hotlink protection solves a problem these scans don't have.
A week later the numbers I'll look at are simple: whether rate limit events from home ISPs stay near zero (before the fix, about 3 of 69 blocked IPs were residential), and whether my own pages still load in one go.