{"course":{"slug":"building-apps-with-ai","title":"Building Apps with AI","url":"https://addaly.com/learn/building-apps-with-ai","level":"beginner","track":"build"},"lesson":{"slug":"rate-limits-and-the-sign-up-flood","title":"Rate limits, and the sign-up flood","url":"https://addaly.com/learn/building-apps-with-ai/rate-limits-and-the-sign-up-flood","position":74,"of":87,"minutes":8,"answer":"Any route a script can call will be called ten thousand times, so the expensive and the account-creating routes get a bot check before sign-in and a per-account limit after it.","keyIdea":"Any route a script can call will be called ten thousand times, so the expensive and the account-creating routes get a bot check before sign-in and a per-account limit after it, and the per-address limit is set knowing that one address can be a whole city."},"body":{"format":"markdown","text":"## What arrives once the site is public\n\nWithin days of any site becoming reachable, automated traffic finds it: scanners probing for known paths, scripts creating accounts, scrapers pulling every page, and (if there is a route that costs money to serve) somebody calling that route in a loop. None of this is personal. It is the background weather of the internet, and a generated app is built as if it did not exist.\n\nThree routes need protecting first: sign-up, anything that sends email or SMS, and anything that calls a model.\n\n## The sign-up flood\n\nA registration form with no bot check will receive a thousand accounts overnight. Each one may trigger a welcome email, consume a free-tier seat, and, if there is a per-account AI allowance, get twenty free model calls. The cost lesson in the previous block warned that a thousand accounts at twenty calls each is twenty thousand calls.\n\nThe defence before sign-in is a bot check. Cloudflare Turnstile is free, does not show puzzles to most people, and takes fifteen minutes to wire: a widget on the form produces a token, and the sign-up route verifies the token with Cloudflare's endpoint before creating the account. hCaptcha is the free alternative. Neither is perfect (paid solving services exist) and both remove the effortless case, which is most of the flood.\n\nBeside it, the sending routes. Password reset, magic link, invite, contact form: each sends an email on request, and an unprotected one is a spam relay with your domain's reputation attached. Bot check, plus a limit per address and per target email, plus a delay: a reset link is not needed within a second of the last one.\n\n## Limits, and where they apply\n\nA rate limit is a counter with a window: at most N requests per key per period. The key is the whole design.\n\n**Before sign-in, the key is the address.** It is all you have. Set it generously, for the reason in this lesson's question: in much of the world, mobile carriers and offices put thousands of people behind a handful of public addresses through carrier-grade NAT. A limit of twenty a minute per address blocks a city's worth of honest users on one network while a bot with a pool of addresses sails through. Per-address limits are for stopping the crude flood, so set them high (hundreds a minute) and rely on the bot check for the rest.\n\n**After sign-in, the key is the account.** Now the limit can be tight, because it is the person and not the network. Twenty model calls a day, ten uploads an hour, whatever the feature warrants, counted in your own table as the cost lesson showed.\n\n**For the expensive route, both.** An address limit to stop the loop and an account limit to stop the patient abuser.\n\n## Implementing one\n\nOn a serverless host, a counter has to live somewhere shared: a Redis-like store (Upstash has a free tier and a ready-made limiter library), a Durable Object on Cloudflare, or a row in your own database with an atomic increment. In-memory counters in a serverless function reset with every process and protect nothing.\n\nThe usual algorithm is a token bucket: each key has a bucket that refills at a fixed rate and holds a maximum; a request takes a token or is refused. It permits short bursts and enforces an average, which is how people actually behave. A limiter that fails (the store is unreachable) should fail open with a log line rather than refuse everyone; an outage of the limiter should not be an outage of the app.\n\nReturn 429 with a `Retry-After` header. Well-behaved clients and your own front end read it; the front end can say \"too many requests, try again in a minute\" instead of showing a generic error.\n\n> Figure: The key decides how tight the limit can be. A two by two grid, rows Before sign-in and After sign-in, columns What the key is and How tight: Before sign-in and What the key is: IP address; Before sign-in and How tight: Generous; After sign-in and What the key is: The account; After sign-in and How tight: Tight. Carrier-grade NAT puts thousands of people behind one public address, so a tight per-address limit blocks a city while a bot with a pool of addresses sails through. Tightness is bought by knowing who is asking.\n\n## What a limit costs\n\nEvery limit refuses somebody honest eventually: the teacher importing forty students, the user on a shared network, the person who pressed the button ten times because the page was slow. Watch the 429 log for the first month. A key that hits the limit constantly is either a bot, in which case the limit is working, or a real pattern of use you did not anticipate, in which case the limit is the bug.\n\n## The scraper\n\nPublic pages will be scraped. This is not harmful in itself, and it can be expensive if each page costs a database query, which is where caching and static rendering, covered in the cloud course, earn their place. Block only the obviously abusive (thousands of pages a minute from one key) and let the rest read; a public page is public."},"check":{"question":"A per-IP limit of 20 requests a minute is added to every route. The next morning, users in one Indian city report the app refusing them constantly while they are barely using it. Why?","options":[{"text":"Mobile networks there put thousands of subscribers behind a few shared public addresses, so twenty people on one carrier exhaust the limit between them.","correct":true},{"text":"The limit is too generous and bots in that city are using up the allowance before real users can.","correct":false,"tempting":"Bots would trip the limit everywhere, not in one city; the pattern points to the address, not the traffic."},{"text":"The limiter's clock is in the server's time zone, so the one-minute windows are misaligned for that region and users there are being counted against a window that never resets.","correct":false,"tempting":"A window is a duration, not a time of day; time zones change nothing about how many requests fit in sixty seconds."},{"text":"The users' browsers are retrying failed requests automatically, which multiplies their real usage.","correct":false,"tempting":"Retries would appear everywhere the app is used; only one region is affected, which a shared address explains and retries do not."}]},"prev":{"slug":"xss-and-the-dangerously-named-prop","title":"Cross-site scripting, and the prop with the honest name","url":"https://addaly.com/learn/building-apps-with-ai/xss-and-the-dangerously-named-prop"},"next":{"slug":"uploads-that-run","title":"Uploads that run","url":"https://addaly.com/learn/building-apps-with-ai/uploads-that-run"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Rate limits, and the sign-up flood', in Building Apps with AI. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/building-apps-with-ai/rate-limits-and-the-sign-up-flood (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Rate limits, and the sign-up flood","courseTitle":"Building Apps with AI","module":{"index":8,"slug":"the-holes-ai-code-has-by-default","title":"The security holes AI code has by default"},"answer":null,"keyIdea":"Any route a script can call will be called ten thousand times, so the expensive and the account-creating routes get a bot check before sign-in and a per-account limit after it, and the per-address limit is set knowing that one address can be a whole city.","hero":{"kind":"figure","figure":{"t":"figure","label":"Figure","figure":{"cols":["What the key is","How tight"],"rows":["Before sign-in","After sign-in"],"type":"matrix","cells":[["IP address","Generous"],["The account","Tight"]],"title":"The key decides how tight the limit can be","caption":"Carrier-grade NAT puts thousands of people behind one public address, so a tight per-address limit blocks a city while a bot with a pool of addresses sails through. Tightness is bought by knowing who is asking.","cellLabels":[["all you have","hundreds a minute"],["a person","twenty calls a day"]]},"alt":"Figure: The key decides how tight the limit can be. A two by two grid, rows Before sign-in and After sign-in, columns What the key is and How tight: Before sign-in and What the key is: IP address; Before sign-in and How tight: Generous; After sign-in and What the key is: The account; After sign-in and How tight: Tight. Carrier-grade NAT puts thousands of people behind one public address, so a tight per-address limit blocks a city while a bot with a pool of addresses sails through. Tightness is bought by knowing who is asking."}},"blocks":[{"t":"h2","id":"what-arrives-once-the-site-is-public","runs":[{"text":"What arrives once the site is public"}]},{"t":"p","runs":[{"text":"Within days of any site becoming reachable, automated traffic finds it: scanners probing for known paths, scripts creating accounts, scrapers pulling every page, and (if there is a route that costs money to serve) somebody calling that route in a loop. None of this is personal. It is the background weather of the internet, and a generated app is built as if it did not exist."}]},{"t":"p","runs":[{"text":"Three routes need protecting first: sign-up, anything that sends email or SMS, and anything that calls a model."}]},{"t":"h2","id":"the-sign-up-flood","runs":[{"text":"The sign-up flood"}]},{"t":"p","runs":[{"text":"A registration form with no bot check will receive a thousand accounts overnight. Each one may trigger a welcome email, consume a free-tier seat, and, if there is a per-account AI allowance, get twenty free model calls. The cost lesson in the previous block warned that a thousand accounts at twenty calls each is twenty thousand calls."}]},{"t":"p","runs":[{"text":"The defence before sign-in is a bot check. Cloudflare Turnstile is free, does not show puzzles to most people, and takes fifteen minutes to wire: a widget on the form produces a token, and the sign-up route verifies the token with Cloudflare’s endpoint before creating the account. hCaptcha is the free alternative. Neither is perfect (paid solving services exist) and both remove the effortless case, which is most of the flood."}]},{"t":"p","runs":[{"text":"Beside it, the sending routes. Password reset, magic link, invite, contact form: each sends an email on request, and an unprotected one is a spam relay with your domain’s reputation attached. Bot check, plus a limit per address and per target email, plus a delay: a reset link is not needed within a second of the last one."}]},{"t":"h2","id":"limits-and-where-they-apply","runs":[{"text":"Limits, and where they apply"}]},{"t":"p","runs":[{"text":"A rate limit is a counter with a window: at most N requests per key per period. The key is the whole design."}]},{"t":"p","runs":[{"text":"Before sign-in, the key is the address.","b":true},{"text":" It is all you have. Set it generously, for the reason in this lesson’s question: in much of the world, mobile carriers and offices put thousands of people behind a handful of public addresses through carrier-grade NAT. A limit of twenty a minute per address blocks a city’s worth of honest users on one network while a bot with a pool of addresses sails through. Per-address limits are for stopping the crude flood, so set them high (hundreds a minute) and rely on the bot check for the rest."}]},{"t":"p","runs":[{"text":"After sign-in, the key is the account.","b":true},{"text":" Now the limit can be tight, because it is the person and not the network. Twenty model calls a day, ten uploads an hour, whatever the feature warrants, counted in your own table as the cost lesson showed."}]},{"t":"p","runs":[{"text":"For the expensive route, both.","b":true},{"text":" An address limit to stop the loop and an account limit to stop the patient abuser."}]},{"t":"h2","id":"implementing-one","runs":[{"text":"Implementing one"}]},{"t":"p","runs":[{"text":"On a serverless host, a counter has to live somewhere shared: a Redis-like store (Upstash has a free tier and a ready-made limiter library), a Durable Object on Cloudflare, or a row in your own database with an atomic increment. In-memory counters in a serverless function reset with every process and protect nothing."}]},{"t":"p","runs":[{"text":"The usual algorithm is a token bucket: each key has a bucket that refills at a fixed rate and holds a maximum; a request takes a token or is refused. It permits short bursts and enforces an average, which is how people actually behave. A limiter that fails (the store is unreachable) should fail open with a log line rather than refuse everyone; an outage of the limiter should not be an outage of the app."}]},{"t":"p","runs":[{"text":"Return 429 with a "},{"text":"Retry-After","code":true},{"text":" header. Well-behaved clients and your own front end read it; the front end can say “too many requests, try again in a minute” instead of showing a generic error."}]},{"t":"h2","id":"what-a-limit-costs","runs":[{"text":"What a limit costs"}]},{"t":"p","runs":[{"text":"Every limit refuses somebody honest eventually: the teacher importing forty students, the user on a shared network, the person who pressed the button ten times because the page was slow. Watch the 429 log for the first month. A key that hits the limit constantly is either a bot, in which case the limit is working, or a real pattern of use you did not anticipate, in which case the limit is the bug."}]},{"t":"h2","id":"the-scraper","runs":[{"text":"The scraper"}]},{"t":"p","runs":[{"text":"Public pages will be scraped. This is not harmful in itself, and it can be expensive if each page costs a database query, which is where caching and static rendering, covered in the cloud course, earn their place. Block only the obviously abusive (thousands of pages a minute from one key) and let the rest read; a public page is public."}]}],"check":{"question":[{"text":"A per-IP limit of 20 requests a minute is added to every route. The next morning, users in one Indian city report the app refusing them constantly while they are barely using it. Why?"}],"options":[{"text":[{"text":"Mobile networks there put thousands of subscribers behind a few shared public addresses, so twenty people on one carrier exhaust the limit between them."}],"correct":true,"tempting":null},{"text":[{"text":"The limit is too generous and bots in that city are using up the allowance before real users can."}],"correct":false,"tempting":[{"text":"Bots would trip the limit everywhere, not in one city; the pattern points to the address, not the traffic."}]},{"text":[{"text":"The limiter’s clock is in the server’s time zone, so the one-minute windows are misaligned for that region and users there are being counted against a window that never resets."}],"correct":false,"tempting":[{"text":"A window is a duration, not a time of day; time zones change nothing about how many requests fit in sixty seconds."}]},{"text":[{"text":"The users’ browsers are retrying failed requests automatically, which multiplies their real usage."}],"correct":false,"tempting":[{"text":"Retries would appear everywhere the app is used; only one region is affected, which a shared address explains and retries do not."}]}],"order":[2,1,0,3],"why":null},"prev":{"slug":"xss-and-the-dangerously-named-prop","title":"Cross-site scripting, and the prop with the honest name","minutes":8},"next":{"slug":"uploads-that-run","title":"Uploads that run","minutes":8}}}