July 11, 2026
Limit Overrun Race Conditions Explained
A limit overrun race condition is a business logic bug where an app checks a limit once, acts on that check, and an attacker fires many…

By Riyandhiman
6 min read
A limit overrun race condition is a business logic bug where an app checks a limit once, acts on that check, and an attacker fires many requests into the tiny window before the state updates. It is a time of check to time of use flaw, TOCTOU for short, and it is how a single use coupon gets redeemed twenty times or a balance gets spent twice. The check passes, the app acts, and by the time the record is written several requests have already slipped through.
The check then act window
Most limit enforcement follows the same two steps. First the code reads some state and checks it against a rule: does this coupon still have a use left, does this account have enough balance, has this user stayed under their quota. Then, if the check passes, the code acts: it applies the discount, moves the money, records the usage. The gap between reading the state and writing the new state is the window. It is usually a few milliseconds, but it is real time on the clock, and during it the stored value has not changed yet.
One request at a time, this is fine. The first request reads, checks, acts, and writes before the next one arrives. The trouble starts when two or more requests land inside that same window. Each one reads the old value, each one sees a limit that has not been spent yet, each one passes the check, and each one acts. The app meant to allow one action and allowed many at once.
Why single request testing misses a limit overrun race condition
A tester who sends one redeem request, sees it work, then sends a second and sees it rejected will conclude the limit holds. That is the normal path, and on the normal path the code is correct. The bug only appears under concurrency. You have to send the requests close enough together that they overlap in the window, which means firing them in parallel, not one after another. A scanner that walks endpoints one call at a time, or a person clicking through a flow, will never produce the overlap, so the flaw stays invisible to them.
This is what makes timing bugs slippery. The input to every request is identical and completely valid. There is no strange payload, no injection string, no malformed field. Twenty honest looking requests, each one exactly what the API expects, together break a rule that any single one of them respects.
A concrete example: one coupon, twenty redemptions
Picture a typical SaaS app, call it Acme Notes, running a launch promotion. It has a coupon SAVE50 that each account may redeem once. The redeem endpoint looks like this in plain terms:
POST /api/coupon/redeem def redeem(user, code): coupon = db.find(code) if coupon.times_used >= coupon.max_uses: # the check return "already used" apply_discount(user, coupon) coupon.times_used = coupon.times_used + 1 # the act db.save(coupon) return "ok"POST /api/coupon/redeem def redeem(user, code): coupon = db.find(code) if coupon.times_used >= coupon.max_uses: # the check return "already used" apply_discount(user, coupon) coupon.times_used = coupon.times_used + 1 # the act db.save(coupon) return "ok"Read one at a time this is correct. The attacker does not read it one at a time. They send twenty copies of the same request at the same instant:
fire 20 requests together: POST /api/coupon/redeem { "code": "SAVE50" } POST /api/coupon/redeem { "code": "SAVE50" } POST /api/coupon/redeem { "code": "SAVE50" } ... 17 more, all at oncefire 20 requests together: POST /api/coupon/redeem { "code": "SAVE50" } POST /api/coupon/redeem { "code": "SAVE50" } POST /api/coupon/redeem { "code": "SAVE50" } ... 17 more, all at onceAll twenty hit the server before any of them finishes writing. Every request runs db.find(code) and reads times_used as 0. Every request compares 0 against max_uses of 1, so every check passes. Every request applies the discount and then writes times_used = 1. The final stored value is 1, which looks perfectly consistent, but the discount was applied twenty times. The same shape turns a gift card into one that pays out twice, a withdrawal limit into a way to drain an account, and a one per customer quota into an unlimited one.
Each request was valid on its own. The rule was broken by the space between the check and the act, not by any one message.
Why it is a logic flaw, not an input flaw
Input flaws come from data the app should not have trusted: a script tag, an SQL fragment, a path that climbs out of a folder. You defend against those by validating and encoding what comes in. A limit overrun race condition has none of that. The data is clean. The flaw lives in an assumption the code makes about itself, that a check it just ran still holds a moment later when it acts. Under load that assumption is false, and no amount of input filtering touches it. This is why it sits in the family of business logic vulnerabilities: it breaks a rule about how the app is supposed to behave, not a rule about what the app is allowed to receive. We take apart more of these in our attack teardowns.
Preventing limit overrun
The fix is always the same idea stated in different ways: make the check and the act happen as one indivisible step, so no other request can squeeze in between them. There are several practical ways to do that.
- Atomic database operations. Let the database do the check and the update in a single statement instead of reading in the app and writing later. A guarded update like
UPDATE coupons SET times_used = times_used + 1 WHERE code = 'SAVE50' AND times_used < max_useschecks and increments at once. Twenty of these run in a line, and only the ones that still satisfy the condition change a row. Look at how many rows each call actually updated to know whether it won. - Row locking. Wrap the read and the write in a transaction and lock the row while you work on it, with a pattern like
SELECT ... FOR UPDATE. The first request holds the lock, does its check and act, and only then releases it. The others wait their turn and see the updated value, so their check fails honestly. - Idempotency keys. Give each intended action a unique key that the client sends, and store it. If two requests carry the same key, the second is recognized as a repeat and returns the first result instead of acting again. This stops accidental double submits and stops an attacker replaying the same intent many times.
- Single flight per user. Serialize sensitive actions for a given account so only one runs at a time. A short lived lock keyed on the user id, or a queue that processes one request per user in order, removes the overlap that the attack depends on.
Rate limiting does not fix this. An attacker needs only a handful of requests inside one window, well under most caps, and the requests look like normal traffic. The real fix is to close the gap between the check and the act.
How to test for it
Find every place the app checks a limit and then changes state: coupons, balances, quotas, invites, one time actions of any kind. For each one, send a burst of identical valid requests in parallel and count how many succeeded. If more than the limit went through, the window is open. The test is not about crafting a clever payload. It is about the timing assumption the code makes, and about proving that assumption wrong under concurrency.
This is the kind of timing and logic assumption an autonomous security researcher is built to test, because it is invisible to single request scanners and only shows up when many valid requests overlap. An early, honest signal: a frontier model drove the full methodology on its own and identified and verified real access control and injection issues in test applications it had not seen before. You can read more about the approach on our about page.
Frequently asked questions
What is a limit overrun race condition?
It is a business logic flaw where an app checks a limit once and then acts, and an attacker fires many requests in the same tiny window so several pass the check before the state updates. It is a time of check to time of use, or TOCTOU, problem.
What kinds of limits does it break?
Anything checked then acted on, such as redeeming a single use coupon many times, withdrawing or transferring a balance more times than allowed, using a gift card twice, or exceeding a per user quota.
Why do single request tests miss it?
The bug only appears when requests overlap. One request at a time always sees a correct balance, so a scanner that sends requests in sequence never triggers the window where several reads happen before the first write lands.
How do you prevent a limit overrun race condition?
Make the check and the update one atomic database operation, use row locking or conditional updates, add idempotency keys, and serialize sensitive actions per user so only one runs at a time.
Put an autonomous researcher on your own systems
UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.
Originally published at https://security.unboundcompute.com on July 11, 2026.