docs

Race conditions (RatRace)

Land many requests in the same instant to beat the gap between check and use — single-packet, last-byte, barrier, or GraphQL-batch sync, with calibration, capture-inject, and state-verify to turn a window into a confirmed catch. (Pro)

A race condition is the window between when an app checks something and when it acts on it: redeem a coupon twice, withdraw the same balance in parallel, beat a rate limit by arriving all at once. RatRace is Hugin's engine for landing many requests inside that window. It is a Pro feature (the read-only views — sessions, results, discovered endpoints — are reachable in Community; firing attacks needs Pro).

The RatRace view firing a synchronized batch and its results
Land many requests in the same instant — single-packet, last-byte, or barrier sync — to exploit the gap between check and use.

The sync engines

Each engine packs the burst onto the wire a different way. Pick one, or leave it on auto — RatRace routes a graphql/gql endpoint to GraphQL batch, an HTTPS target to last-byte, and a plain-HTTP target to barrier.

Single-packet

Multiplex many HTTP/2 requests over one connection and release the completing frames together, so the server sees them at the same instant — the most reliable way to defeat network jitter. HTTP/2 only.

Last-byte sync

Send all but the final bytes of each request over raw TCP, then release the held tails together — the HTTP/1.1 equivalent, and the auto choice for HTTPS because it works whether or not the server speaks HTTP/2.

Barrier

Hold a batch of requests at a barrier and release them together, for flows that need true parallelism. Simplest, no TLS handshake to warm — the auto choice for plain HTTP.

GraphQL batch

Pack many mutations into one request body so the server runs them in parallel. One round trip, zero network jitter — the tightest window of all when the target accepts batched operations.

The burst size is concurrency (default 10, capped at 256) repeated over rounds (default 1). A real race converges in the tens, not the thousands.

Run it

  1. Find a candidate

    An action that checks a limit then mutates state — a balance, a quota, a one-time token, an idempotency key. discover ranks a list of URLs by how race-prone they look (payment, coupon/referral, balance transfer, inventory, MFA, password-reset, subscription-downgrade and more); param-hunt ranks the parameters on a single endpoint.

  2. Send it as a batch

    Take the request to RatRace, set the batch size, and fire with single-packet, last-byte, or auto. You can also send a single-packet batch straight from Repeater (Atomic Send) or Intruder.

  3. Read the outcome

    Two successes where one was allowed is the bug. Check the resulting state, not just the responses — turn on calibration or state-verify (below) to make the catch unarguable.

Attack modes

Beyond a raw test, RatRace ships preset modes that shape the burst for a specific bug class. Each is a one-call wrapper over the same engines.

detect

The "just find me a race" sweep: auto-selects the engine, warms up, and runs a few rounds.

limit-bypass

Pushes concurrency up (≥ 20) over several rounds to beat a per-request rate-limit or quota by arriving all at once. Pair it with calibration to confirm the bypass.

state-fuzz

Mutates the payload across the burst to trip invalid state transitions — parallel writes that the state machine never expected together.

cache-race

Varies cache-related headers across the burst to catch a CDN or cache-poisoning race.

websocket-race

Fires N concurrent WebSocket upgrade handshakes at one endpoint — duplicate session creation and auth-check TOCTOU during the upgrade. (Frame racing over an already-open socket is out of scope.)

microservice

Fires the same race across several service URLs in one run, for cross-service consistency bugs where two backends disagree under load.

param-hunt

Recon, free in Community: ranks the parameters on an endpoint most likely to be race-sensitive for a given bug category, so you race the right field.

Protocol races

Two protocols get dedicated handlers with their own config, because the race lives in the protocol's own flow rather than a single request.

GraphQL subscription

Races concurrent subscription create/cancel for authorization bypass, double-delivery of events, and resource exhaustion.

OIDC logout

Races the logout flow: back-channel and front-channel logout, RP-initiated end-session, token revocation, and the logout-versus-resource-access TOCTOU (is the session still usable for a beat after logout?). Needs the full logout config — endpoints plus the session/token material — so drive it from the API or agent.

Read the verdict

Every round is classified into one signal; the strongest signal across all rounds is the session verdict.

  • CLEAN_CATCH — a confirmed race. Divergent statuses with some succeeding, a database race error in a body, or a calibrated limit bypass.
  • SILENT_SUCCESS — the burst changed server state with no error in sight, confirmed by state-verify. The double-spend receipt.
  • LOGICAL_RACE — successful responses that disagree with each other, a sign the parallel requests diverged the logic.
  • DIRTY_CATCH — a lead to chase, not yet proof. Includes the key case below.
  • RATE_LIMITED — the target's guard held across the burst (most responses 429).
  • FALSE_POSITIVE / NO_SIGNAL — nothing actionable this run.

Identical successes mean different things by method. On a safe method (GET/HEAD/OPTIONS) a burst of byte-identical 200s is a true negative (FALSE_POSITIVE). On POST/PUT/PATCH/DELETE the same burst is flagged DIRTY_CATCH — the side effect may have been applied N times (silent double-spend) — and RatRace points you at state-verify to confirm it.

The verdict is built from divergence signals across the burst:

Response, status, and length divergence

Distinct body hashes, distinct status codes, or body lengths varying by more than 10 % across requests that should have behaved identically.

Backend-node divergence

Responses grouped by Server / X-Served-By that disagree — two backend nodes took different paths under the same concurrent load.

Database race errors

The burst surfaced a deadlock, a unique-constraint failure, a serialization error, a Redis WATCH/EXEC conflict, or a locked SQLite — read straight out of the response body. A confirmed database-level race.

Catch a limit bypass with calibration

This turns "the rate limit holds for one request but folds under concurrency" into a clean, defensible catch.

Enable calibrate and RatRace sends one non-concurrent request first, then the burst. If that single request is rejected — a 429, a one-time-use 409/403, a 4xx balance or precondition error — but two or more concurrent requests succeed, that is a per-request guard bypassed under load. It is reported as limit_bypass and promoted straight to CLEAN_CATCH, even when every winning response is byte-identical and the burst has no internal divergence.

Calibration is off by default: the baseline request is one real extra invocation, so only enable it where priming the endpoint once is safe.

Chain a multi-step race

Most real races need a setup step first — create the basket, mint the draft order, grab a fresh token. Configure a prerequisite request that runs once before the burst. It has two uses:

  • Pure setup — create the resource the race targets, then race the next step (add-to-cart, then race checkout).
  • Capture-and-inject — pull a value out of the prerequisite's JSON response by path (for example data.token) and thread it into every burst request: as a named header, optionally templated (Bearer {{CAPTURE}}), and/or by substituting the {{CAPTURE}} marker anywhere in the burst's URL, body, or header values.

A value captured from the prerequisite (often a fresh bearer) is never written into the saved session.

Prove the double-spend with state-verify

Responses lie; state is proof. A double-spend often returns N identical 200s and nothing else. Point state-verify at a read endpoint — a wallet balance, an order count — to fetch the state before and after the burst. Narrow the comparison to the field that matters with a JSON path (data.wallet.balance) so a volatile timestamp or CSRF token does not produce noise.

A confirmed change on the targeted field upgrades the verdict to SILENT_SUCCESS — the before/after diff you put in the report.

Tune the window

When a burst comes back empty, the timing is usually the problem, not the target.

Warmup

Primes connection pools and server caches with a short ramp (1, 2, 4, 8 requests) before the real burst. On by default; the quick and websocket modes turn it off for one-shot endpoints.

Jitter

Adds a small random delay per request after the barrier (default up to 5ms). A perfectly-simultaneous burst can all collide on a single-writer lock and roll back together; a few milliseconds of jitter re-opens the read-then-write window.

Window sweep

On by default. When a non-idempotent burst comes back NO_SIGNAL or FALSE_POSITIVE, RatRace re-probes on a 5 / 25 / 75ms jitter ladder and stops at the first rung that catches. The 25ms rung is what opens OWASP Juice Shop's wallet double-spend.

Last-byte controls

Tune the last-byte engine three ways: the sync method (Nagle-coalesced, single-packet push, or H2-multiplex timing), the release strategy (simultaneous, microsecond-staggered, or a second confirmation barrier for the tightest grouping on multi-core), and the hold point — how much of each request to send before withholding the rest (default sends 99 %, holds the final 1 %).

Race with auth: drive it from the API or agent

The GUI Send to RatRace prefills the URL, method, and body of the flow — but not its Authorization or Cookie headers, so a race launched that way runs unauthenticated. For an authenticated race, set the headers yourself by driving RatRace from the API or an MCP agent, where the full header set goes on the wire.

Pivot a BAC finding straight into a race

A broken-access-control verdict where a low-privilege identity got the same response as a high-privilege one is a prime TOCTOU target — the authorization check may be racing the resource lookup. The BAC tool's to_ratrace action turns each High/Critical finding into a ready race candidate (the flow to replay, the endpoint, and the rationale), so you confirm the race without rebuilding the request by hand.

A race attack can double-spend or corrupt real data. Only run it where you are authorised, and prove the window on a test account or a non-destructive action first.

Last updated 2026-06-17.