August 24, 2026
Race Conditions Aren’t Rare. Your Tooling Was Just Late.
30-Second Version: Everyone treats race conditions as exotic — a luck bug reserved for specialists with rare intuition. That reputation was…

By Raj Namdev
6 min read
30-Second Version:_ Everyone treats race conditions as exotic — a luck bug reserved for specialists with rare intuition. That reputation was earned honestly, and it's also a couple of years out of date. The single-packet attack quietly deleted the reason races were hard to land remotely, and the bugs were sitting in ordinary check-then-act code the entire time. What's left isn't luck. It's picking a check worth breaking and proving what it costs._
Everyone treats race conditions as exotic — a luck bug for specialists. The single-packet attack quietly deleted the reason they were hard, and the bugs were everywhere the whole time
Ask a room of bug bounty hunters how to find race conditions and you'll hear the same weather report: hard to time, easy to miss, mostly luck. The top-voted discussions on r/bugbounty treat them like a discipline hunt — how do you systematically approach race conditions? — as if the answer is a rare intuition developed over years.
That reputation was earned honestly. It's also a couple of years out of date.
The bugs were never rare. What was rare was the ability to land twenty requests on the same millisecond from another continent. That was a networking problem, not a security problem — and it got solved.
The Excuse That Used to Be True
Race hunting's bad reputation comes from one physical fact: the internet is slow in unpredictable ways. Fire 20 identical requests in parallel and network jitter — unpredictable, network-induced delays in packet transmission, as PortSwigger's research frames it — scatters their arrival across tens or hundreds of milliseconds. The server processes them in whatever order they land, the vulnerable window closes after the first one, and requests two through twenty arrive at a finished party.
So hunters concluded the bug class was a lottery. Test locally against a dev instance and the race lands every time — jitter over localhost is near zero. Ship the same test at a production target and it never lands. The obvious, wrong lesson: the target isn't vulnerable. The correct lesson: the target is a round-trip away, and the requests were never actually simultaneous.
That was the consensus. And until recently, it was even fair.
What Changed: One Packet
The fix arrived in two steps. The 2024 research "Smashing the State Machine" gave Turbo Intruder last-byte synchronization: queue every request with the final byte withheld, then release all of them at once. Most of the jitter disappears, because the part of each request that actually matters — the byte that completes it — now travels together.
Then the single-packet attack finished the job on HTTP/2. Queue 20–30 requests on one connection, each missing its last fragment. Release them, and the operating system groups those fragments into a single TCP packet. The server receives them all at effectively the same instant, regardless of how much network jitter delayed their delivery along the way. The research's own framing is the entire argument: it makes remote race conditions local. The thing that made production targets feel immune simply doesn't exist anymore.
The code is almost embarrassingly small. This is the canonical race template from Turbo Intruder's documentation, runnable today in Burp Suite:
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1,
engine=Engine.BURP2)
# queue the requests, withholding the final byte of each
for i in range(20):
engine.queue(target.req, gate='race1')
# release every final byte at once — one packet
engine.openGate('race1')
def handleResponse(req, interesting):
table.add(req)def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1,
engine=Engine.BURP2)
# queue the requests, withholding the final byte of each
for i in range(20):
engine.queue(target.req, gate='race1')
# release every final byte at once — one packet
engine.openGate('race1')
def handleResponse(req, interesting):
table.add(req)One connection. One gate. Twenty requests becoming one packet. No threading, no retry loops, no hoping. If the target speaks HTTP/2, Burp Repeater's "Send group in parallel" gives the exact same engine from a right-click menu — no code required at all.
The Bugs Were Never Rare
Here's why "rare" was always the wrong word. A race condition isn't an exotic defect some unlucky developer accidentally introduces. It's what happens any time a server checks a rule and then enforces it as two separate steps. Check, then act. Redeem a gift card: check it's unused, then mark it used. Verify an email: check no account exists, then create one. Enforce "one per user": count, then insert.
Every one-per-user rule, every single-use code, every uniqueness constraint, every rate limit, every transfer-then-update balance flow — all of it is check-then-act. The race isn't hiding in some weird code path. It's the default shape of feature code, and the only real question is whether the window between check and act is wide enough to land a second request inside it.
The disclosed-record evidence agrees. The race-condition list in the hackerone-reports dataset runs just 38 entries — but look at where they live: gift card redemption, 2FA enforcement, duplicate payments, OAuth 2 implementations, email verification, faucet payouts, loyalty claims, follower limits. Eight different programs, zero shared code between them. The same bug, everywhere, sitting in the most ordinary features a product has.
That's not the distribution of a rare bug. That's the distribution of a common bug that almost nobody was actually landing.
So Why Is the Disclosed Pool Still Tiny?
This is where the counterargument lives, so it's worth taking seriously.
Part of it is lag — habits formed in the jitter era haven't caught up to the gate yet. Part of it is that the pool measures reports, not bugs that exist. But the biggest part is the part nobody wants to hear: landing the race was never the whole problem. What you race, and what you can prove it actually costs — that's still judgment, and the dataset prices it precisely.
Three disclosed reports, same technique class, three different outcomes. A Cosmos faucet race that mints the same coins repeatedly (report #1438052): Critical, $5,000. A verification-check bypass on Tools for Humanity (report #2110030): High, $3,000. And the gift-card multi-redeem on Reverb (report #759247) — the most-upvoted race report in the entire dataset, a clean writeup with Turbo Intruder steps showing exactly how to buy once and redeem forever: High severity, $0.
Then there's the fourth outcome, the instructive one. A hunter raced organization creation on WakaTime (report #3248712), created multiple organizations under the same name, and reported it — noting it could lead to confusion, broken business logic, or potential misuse. The program closed it as Not a Vuln. The race landed cleanly. The impact never existed. No amount of tooling can write that part of the report for you.
So the honest scorecard: transport — solved. Target selection and impact proof — still entirely yours. The single-packet attack deleted the luck. It never touched the thinking.
What the Modern Method Actually Looks Like
Strip away the mythology and the field workflow is short:
1. Enumerate the checks — every endpoint that says once,
limit, unique, per-user, or single-use.
2. Predict which ones guard something with a real price
attached to it.
3. Probe — queue 20–30 requests behind a gate, release,
diff the responses. N successes where the business
rule promised exactly one is the signal.
4. Prove — show the actual state after the race: the
balance, the extra redemption, the second account
that shouldn't exist at all.1. Enumerate the checks — every endpoint that says once,
limit, unique, per-user, or single-use.
2. Predict which ones guard something with a real price
attached to it.
3. Probe — queue 20–30 requests behind a gate, release,
diff the responses. N successes where the business
rule promised exactly one is the signal.
4. Prove — show the actual state after the race: the
balance, the extra redemption, the second account
that shouldn't exist at all.Two field notes that save an afternoon. If the target runs PHP, its default session handler file-locks the session for each request, quietly serializing a "parallel" attack into a polite queue — sending each request with a different session token makes that lock evaporate. And if a race needs two different endpoints to land together, warm the connections first; where the target rate-limits aggressively, abusing the rate limiter itself can help — tripping it adds a server-side delay that holds requests in the window longer than they'd otherwise sit.
None of that is intuition. None of it is luck. It's a map of where checks live, a gate, and a diff.
The Window Is Open
For twenty years the industry's mental model of race hunting was a lottery ticket — maybe the packets align, maybe they don't. That model quietly expired the moment final bytes started traveling in one packet together. The mechanics that made races feel "exotic" are now four lines of Python, or a single right-click.
What's left is the part that was always the real work — picking a check worth breaking and proving exactly what it costs. The hunters collecting four-figure checks on faucets and verification flows already made that shift. The ones still calling race conditions lucky are competing against a version of the internet that no longer exists.
Sources
- "The single-packet attack: making remote race conditions local" — PortSwigger Research — jitter numbers, the 20–30-requests-per-packet scale, protocol analysis.
- "Smashing the state machine: the true potential of web race conditions" — PortSwigger Research — last-byte synchronization, sub-states, the systematic methodology.
- "Race conditions" — PortSwigger Web Security Academy — the check-then-act taxonomy, session locking, connection warming, rate-limit delay tactics.
- Turbo Intruder: race conditions — GitHub — the gate mechanism and the race template used above.
- Dataset: reddelexc/hackerone-reports — TOPRACECONDITION list, 38 entries; severity and bounty figures pulled from each cited report's live JSON. All report links verified resolving.
- Reports cited inline: #1438052 (Critical, $5,000), #2110030 (High, $3,000), #759247 (High, $0), #3248712 (Not a Vuln).
- "How do you find high-impact business logic & race conditions?" — r/bugbounty — the consensus this article argues against.