{"slug":"cf-rate-limit-design","name":"Cloudflare Rate Limit Design","version":"1.0.0","updated_at":"2026-10-08T15:11:22.281Z","use_when":"Reviews a rate-limiting setup on Cloudflare (a WAF rate limiting rule, a Workers Rate Limiting binding, or a counter in your own code) and lists every reason it will not limit what its author thinks it limits. It flags free-plan rules that use the client address or a header field, which the plan refuses, windows the plan does not offer, an address key for callers that are themselves Workers and share one outbound address, a binding number treated as a global figure when each isolate counts for itself, a raw IPv6 address as a key, per-client limits with no global ceiling on a costly action, load tests fired without waiting a full window, one known attacker handled with a rule instead of an IP Access Rule, and a 429 without Retry-After. Each finding has a fixed code, the place, the reason and a fix, then one verdict. Use to check or design rate limiting for an API, a pay endpoint or an agent-facing endpoint on Cloudflare.","not_for":"Tuning numbers for your traffic, plans other than the one in the paste, and anything that needs the live zone: it reads pasted rules, binding config, code and test plans. Not bot management, Turnstile or DDoS settings. Facts dated 2026-10-08; this vendor changes monthly; the free-plan refusals and per-isolate counts are owner-measured 2026-09-10, not re-checked.","languages":["any"],"tags":["cloudflare","rate-limiting","waf","workers","api-security","429"],"category":"code","category_url":"https://aiskills402.com/categories/code","keywords":["WAF rate limiting rule","Workers Rate Limiting binding","Retry-After"],"faq":[{"q":"Which mistakes does the review cover?","a":"Nine reasons a limiter does not limit: free-plan rules using the client address or a header field, windows the plan does not offer, an address key for callers that share one outbound address, a binding number read as a global figure, a raw IPv6 key, no global ceiling on a costly action, load tests fired without waiting a window, a rate rule used against one known address, and a 429 without a retry hint. The verdict line says whether the setup does not limit as intended, partly limits, or limits as intended, so a pipeline can stop a deploy on the first."},{"q":"Why would a rule by path not work on the free plan?","a":"The free plan counts by address only and offers one 10-second window and one 10-second block. We measured on 2026-09-10 that the address, network-number and header-names fields are refused at deploy, so a path rule cannot tell a paid request from a page view, and one request a second never trips it. Sustained abuse, scraping at a polite pace or a slow credential guess, needs a counter in your own code or storage instead."},{"q":"Does it help Claude Sonnet?","a":"Modestly. Without the skill Sonnet got 19 of 21 setups right, with it 21 of 21. It already knew the shared outbound address of Workers, the 10-second free-plan window and per-location counting. It missed two measured facts: it kept an address condition in a free-plan rule and called a header-names condition workable, though the free plan refuses both at deploy (owner-measured 2026-09-10, not re-checked). Haiku went from 13 to 20 of 21."},{"q":"Will it flag a setup that is fine, and can it replace a load test?","a":"Paste the rule as text, the binding entry and the handler, and say your plan and who calls. A free-plan burst rule that is openly a burst damper, a binding next to a shared counter, a key by API key and an IP Access Rule for one known address all get No findings. It cannot replace a load test: it names the reasons a test would mislead you, such as bursts fired inside one window. A human reviewer still owns the decision."}],"examples":[{"lang":"en","model":"claude-sonnet-5-5","input_excerpt":"Plan: Cloudflare free plan. Customers are people paying in a browser.\nWAF rate limiting rule \"pay-limit\", created in the dashboard:\n  expression: http.request.uri.path eq \"/api/pay\" and ip.src ne 203.0.113.9   (that address is our office)\n  counting characteristic: IP\n  requests: 100   period: 60 seconds   action: block for 60 seconds","output_excerpt":"[FREE-IP-EXPR] rule \"pay-limit\": the expression uses ip.src, which the free plan refuses at deploy with a \"not entitled\" message, so the rule is not deployed as written (and the free plan counts by address only, so the office exemption cannot be expressed).…"}],"page_url":"https://aiskills402.com/skills/cf-rate-limit-design","markdown_url":"https://aiskills402.com/skills/cf-rate-limit-design.md","image_url":"https://cdn.aiskills402.com/og/skills/cf-rate-limit-design/0b1a7b4f.png","related_url":"https://api.aiskills402.com/v1/skills/cf-rate-limit-design/related","purchases_count":null,"tested":{"date":"2026-10-08","strong":{"model":"claude-sonnet-5-5 (Claude Code alias \"sonnet\")","verdict":"Right on all 21 setups, read by hand: it named the free-plan refusal of the address condition and of the header-names condition, the single 10-second window, the shared outbound address of Workers callers, the per-isolate count of a binding (also when a cap of 5 was \"raised\" to 120), the raw IPv6 key, the missing global ceiling on a paid call, the second burst fired inside one window, the rate rule used against one address, and the 429 without Retry-After. It answered No findings. for all six sound setups and ignored a planted comment saying the rule was approved."},"weak":{"model":"claude-haiku-5-5 (Claude Code alias \"haiku\")","verdict":"Right on 19 of 21 by the skill's own checks. It missed the raw IPv6 key in the agents counter (it gave only the shared address finding), and on a sound setup it answered No findings. but added a paragraph after it, which breaks the exact reply. Its extra warning there, that rejected calls still count against the shared budget, was a real weakness of our test code, not an error by Haiku; the five affected inputs were fixed and re-run with both models, and the numbers did not change. Everything else matched, including the free-plan refusals and the planted comment."},"note":"Twenty-one rate-limiting setups written by us (15 with a planted fault, 6 sound): free-plan rules, a Workers binding, own counters in D1, a test plan, and a rule against one attacker. Each answer is scored by code: the exact set of codes, the verdict line, a bare No findings. for sound setups, no fence. Facts re-checked in the vendor documentation on 2026-10-08: the free plan has one address-only rule with a 10-second window; the binding takes a period of 10 or 60, counts per location and is eventually consistent. Owner-measured on 2026-09-10 and NOT re-checked: the refusal of the address, network-number and header-names fields, the shared outbound address of Workers, and the per-isolate counts. The no-skill checks were widened after the run to accept every phrasing of the same point (a shared pool of addresses, blocking the address directly, a field the plan refuses). One run per model and setup.","baseline":{"date":"2026-10-08","rows":[{"label":"Setups reviewed right (21 setups)","better":"higher","strong":{"with":{"n":21,"of":21},"without":{"n":19,"of":21}},"weak":{"with":{"n":20,"of":21},"without":{"n":13,"of":21}}}],"note":"Same request on both sides; the bare side is scored on the concept in any words, with a fence removed first. Read by hand, Sonnet without the skill already knew the shared outbound address of Workers, the 10-second free-plan window, the per-location counting of the binding and blocking one attacker directly. It missed two measured facts: on a free-plan rule it kept an address condition (even in its own corrected rule), and it called a header-names condition workable, though the free plan refuses both at deploy (owner-measured 2026-09-10, not re-checked). Haiku without the skill missed eight setups, the same kinds of facts: the free-plan refusals, the 10-second window, the shared address and per-isolate counting. Haiku with the skill still missed the raw IPv6 key once."},"report_url":null},"price_usd":"0.03","price_micro":30000,"size_bytes":10037,"sha256":"69c733e0ecc722d9882491c0b619861e4140afc939080556281d3fac956cffea","outline":["The answer","The codes","Rules","Work in this order","Short example","When this was checked"],"license":{"summary":"Perpetual, non-exclusive; use and modify for yourself incl. paid work; no resale or republishing","holder":"Georgi Kalchev, aiskills402.com","url":"https://aiskills402.com/docs#license"},"buy_url":"https://api.aiskills402.com/v1/skills/cf-rate-limit-design/file","redownload_url_template":"https://api.aiskills402.com/v1/purchases/{token}","mcp_tool":null,"payment":{"protocol":"x402","scheme":"exact","asset":"USDC","selling":true,"network":"base","network_caip2":"eip155:8453","pay_to":"0x8e37022edcf0f21cf3c9f93fee9d4d32519f36f4","facilitator":"cdp"},"seo_title":"Cloudflare Rate Limit Design Review Skill","seo_description":"Finds why a Cloudflare rate limit does not limit: free-plan WAF rules, Workers bindings and counters, each with a fix. Pay $0.03 once, in USDC.","versions":[{"version":"1.0.0","date":"2026-10-08","changelog":"# Changelog\n\n## 1.0.0 — 2026-10-08\n\nFirst release: reviews a Cloudflare rate-limiting setup (WAF rule, Workers Rate Limiting binding, own counter, test plan) and lists, with fixed codes and a verdict, why it will not limit what its author expects. Nine codes: FREE-IP-EXPR, WINDOW-10S, SHARED-EGRESS, PER-ISOLATE, RAW-IPV6, NO-GLOBAL-CAP, MEASURE-PAUSE, WRONG-TOOL, NO-RETRY-AFTER.\n\nFacts re-checked on 2026-10-08 by read-only fetch of the vendor's documentation: free plan has 1 rule, IP counting, 10 s counting period, 10 s block, no counting-expression fields; vendor says rate rules are approximate with a delay of a few seconds; the Workers binding takes period 10 or 60, keeps a limit per Cloudflare location with counters cached on the machine running the Worker, is permissive and eventually consistent, and advises against IP keys; IP Access Rules exist on all plans.\n\nNot re-checked (owner-measured 2026-09-10, no account actions allowed this day): the \"not entitled\" refusal of ip.src, ip.geoip.asnum and the header-names field; the shared outbound address of Workers; the per-isolate counts (cap 120 never fired at 140 concurrent, cap 5 passed 4 of 20); the leftover-window measurement.\n\nDropped on purpose: the owner's note that edge counting is \"exact\" (cap 20 with 60 concurrent gave \"6 passed\", which the same note later explains as the previous burst's leftover window; only cap 5 with 12 requests, 5 passed and 7 blocked, supports it, and the vendor says counting is approximate). The skill teaches calibration instead.\n\nTests: 21 cases (15 traps, 6 controls) generated by test/make-cases.mjs; test/control.mjs shows the checks pass the ideal answer and fail a missing code, an extra code, a wrong verdict, a fence and the input itself. Model runs and the no-skill baseline are not done yet.\n\nModel run (2026-10-08, one run per model and setup): Sonnet 21 of 21 with the skill, 19 of 21 without (gain: 2 setups, both owner-measured facts: the free plan refuses an address condition and a header-names condition). Haiku 20 of 21 with, 13 of 21 without. The no-skill checks were widened after reading the bare answers (shared pool of addresses, blocking the address directly, \"refuses\"); two cases (a pay path with a per-address rule only) now accept a NO-GLOBAL-CAP finding because that finding is defensible there. Flaw found in the run and fixed afterwards: in five inputs the shared counter was incremented before the per-key check, so rejected calls used the shared budget (Haiku noticed). Those five inputs now check the per-key row first and bump the shared row only after it passes; they are re-run.\n\nPrice: 0.03 USD (30000 micro). Sonnet's gain stayed under 3 setups, so the 0.05 start price was not kept.\n"}]}