Module 16 · Race Conditions in Web Apps

Manish Garg
Manish Garg Associate of (ISC)² · RingSafe
Apr 22, 2026
11 min read
Read as

Last updated: May 1, 2026

100% Free

No signup. No paywall. No catch. One of our 10 most-requested practitioner modules — published in full so anyone can learn for free. We earn through consulting, not by gating knowledge.

See all 10 free modules →

TOCTOU, single-packet attacks, where races hide, Burp testing, transactional + idempotency-key defenses.

Race conditions in web apps occur when two requests, processed concurrently, produce a state that neither would alone. Classic TOCTOU (time-of-check-to-time-of-use) bugs. They turn “redeem this coupon once” into “redeem 50 times if you fire 50 requests at the same instant.” James Kettle’s research on single-packet attacks (2023) made these exploitable at high reliability. This module covers detection, exploitation, and defense.

The classic pattern

# Pseudocode of a vulnerable coupon-redeem endpoint
def redeem(user_id, coupon_code):
    coupon = db.query("SELECT * FROM coupons WHERE code=?", coupon_code)
    if coupon.redeemed_count >= coupon.max_uses:
        return "expired"

    # ... business logic ...
    user.balance += coupon.value
    db.execute("UPDATE coupons SET redeemed_count = redeemed_count + 1 WHERE code=?", coupon_code)
    db.execute("UPDATE users SET balance=? WHERE id=?", user.balance, user_id)

Two requests arrive concurrently. Both pass the redeemed_count < max_uses check before either UPDATE runs. Both increment balance. Coupon redeemed twice, money credited twice.

Where they hide

  • Promo code redemption
  • Withdrawal / transfer endpoints (double-spend)
  • Rate-limiter increment (race past the limit)
  • 2FA verification (race to bypass)
  • Account creation with uniqueness check
  • One-time-use tokens
  • Voting / liking systems
  • Cart / checkout flows that re-validate inventory

The single-packet attack

James Kettle showed that with HTTP/2, you can send the last byte of multiple requests in a single TCP packet. The server processes them in nearly-perfect parallel — the timing window opens microseconds wide.

Tools:

  • Burp Suite Repeater with “Send group in parallel (single-packet)” mode — built-in for race testing
  • Turbo Intruder — Burp extension; programmable for complex race scenarios
  • Project Discovery’s nuclei — limited race testing

Testing — the safe approach

  1. Identify candidate endpoints (state-changing, with check-then-act logic)
  2. In Burp, capture a successful request
  3. Send to Repeater, group-tab
  4. Duplicate the request 20-50 times in the group
  5. Send group in parallel (single-packet)
  6. Compare server responses; check downstream state

If 50 parallel requests produce 50 successful actions when business logic intended 1, you have a race condition.

Real exploitation examples

  • Promo code intended for 1 use, applied 50 times → 50× discount on order
  • Withdrawal with $1000 balance, fired 5 times in parallel → $5000 withdrawn
  • Account creation race producing duplicate username (different DB locking modes)
  • 2FA bypass: the wrong-code-counter check + reset race
  • Voting endpoint: 100 votes from one user when limit was 1

Detection in code review

Look for:

  • Read-then-update sequences without transactional consistency
  • Counter increments via SELECT-then-UPDATE rather than UPDATE with WHERE
  • Application-level locking using flag columns
  • Cache-based deduplication that depends on cache consistency
  • Anything described as “we check if X, then do Y”

Defense patterns

Database-level locking

# SELECT FOR UPDATE — pessimistic lock
BEGIN TRANSACTION;
SELECT * FROM coupons WHERE code='X' FOR UPDATE;  -- locks the row
-- check + decide
UPDATE coupons SET redeemed_count = redeemed_count + 1 WHERE code='X';
COMMIT;

# Atomic UPDATE with conditional WHERE
UPDATE coupons
SET redeemed_count = redeemed_count + 1
WHERE code = 'X' AND redeemed_count < max_uses;
-- If no rows affected, redemption failed; act accordingly

Optimistic concurrency control

# Version-based; no locking needed
SELECT *, version FROM accounts WHERE id=1;  -- v=5
-- ... compute new state ...
UPDATE accounts
SET balance=newbal, version=version+1
WHERE id=1 AND version=5;
-- Affected rows = 0 means another transaction won; retry

Idempotency keys

Client provides a unique key per logical operation. Server stores key → result mapping. Retry with same key returns cached result, doesn’t execute again.

POST /api/transfer
Idempotency-Key: 4f3e2d1c-8b2a-4c5d-9f1e-2a3b4c5d6e7f

# Server: SELECT * FROM idempotency WHERE key=...
# If exists → return stored result
# If not → execute, store result with key (in transaction)

Stripe API popularized this pattern; widely adopted in payment APIs.

Distributed locks

For operations that span multiple services, use Redis-based locking (RedLock pattern) or Zookeeper. Caveats — distributed locks are notoriously hard to get right; prefer transaction-scoped locking when possible.

Single-writer pattern

Some systems route all writes for a particular entity to a single worker (sharded by user_id, account_id). Eliminates concurrency at the application level. Operational overhead but architecturally simple.

Defense at the boundary — rate limiting won’t save you

People sometimes think rate limiting prevents races. It doesn’t — race exploits use single-packet attacks that fit within rate limit windows. Rate limit is for abuse volume, not race conditions.

Detection in production

  • Anomaly metrics: counters going negative, balances exceeding business limits, voting counts above what’s possible
  • Audit log clustering: identical actions within milliseconds from same user
  • Reconciliation jobs: nightly checks that aggregated state matches expectations

Race exploits often produce side effects that show up later — a coupon redeemed 50 times shows up in revenue reports as anomalously low. Catch them in reporting if you missed at the wire.

Common race classes by impact

  • Critical: double-spend on financial transactions, mass coupon abuse, account takeover via parallel password reset
  • High: rate limit bypass enabling further abuse, integrity violations on shared resources
  • Medium: uniqueness bypass on usernames/emails, vote stuffing
  • Low: minor UI inconsistencies from concurrent updates without state damage

Where this leads

Module 17 covers prototype pollution — JavaScript-specific vulnerability class with cascade effects across the application.

🧠
Check your understanding

Module Quiz · 15 questions

Pass with 80%+ to mark this module complete. Unlimited retries. Each question shows an explanation.


⚙ Optimisation · Performance · Security — extended

Practical depth on what to tune, what to harden, and how this maps to Indian regulatory expectations.

Web race conditions — TOCTOU and the single-packet attack

Race conditions in web apps occur when two requests interleave such that the app thinks each is operating on a fresh state, but the second arrives before the first commits. Classic example: redeem-coupon endpoint reads “this user has not redeemed yet”, proceeds; two parallel requests both read the not-yet-redeemed flag, both proceed, attacker gets twice the discount. The single-packet attack (PortSwigger 2023) sends two HTTP/2 requests in one TCP packet, eliminating network jitter that previously made races hard to reproduce. With HTTP/2, races that needed 1000 attempts now fire on the first try.

Common race targetsbalance updates, coupon redemption, withdrawal limits, MFA bypass via concurrent submit, file-uploads with overwrite, account-sign-up email-verification.

Defensive patterns + detection

1Database transactions with proper isolation: SERIALIZABLE or row-level locks on the affected entity.
2Optimistic concurrency control: version column; UPDATE WHERE version = expected.
3Idempotency keys for client-driven retry — duplicates are no-ops.
4Distributed locks (Redis, ZooKeeper, etcd) for cross-instance critical sections.
5Atomic database operations: UPDATE balance = balance - X WHERE balance >= X as one statement — atomic by SQL semantics. Detection: alert on parallel requests from one user that should be sequential; alert on unexpected duplicate side-effects (two coupons redeemed in same second). For Indian fintech: race-condition findings in payment / wallet / lending apps are flagged as P0/P1 by RBI inspectors due to direct financial loss potential.

Distributed race conditions — when locks span machines

Race conditions in distributed systems multiply because one process is no longer the synchronisation primitive. Patterns:

1Distributed lock via Redis / etcd / ZooKeeper — works for short critical sections; expiry handling is subtle (lease + auto-renew).
2Optimistic concurrency with version — UPDATE WHERE version=expected; retry on conflict.
3Single-writer architecture — partition data so one node owns mutations on each key; eliminates cross-node race.
4Database-level constraints — push the invariant down to the DB so the database engine’s ACID properties enforce. Common failure: client-side retry-on-conflict that does not actually re-validate state — both the original and the retry succeed because intermediate state changed. For Indian payment / wallet systems: distributed-locking patterns are now a regulator-relevant control; RBI inspections review architecture for race resilience explicitly.

Idempotency keys — the production pattern that solves the most races

Idempotency keys are a client-supplied identifier that the server uses to deduplicate requests. Pattern: client generates a UUID per logical operation; server records (idempotency_key → result) in a database; subsequent requests with the same key return the recorded result instead of re-executing. Benefits:

1Network-retry safety — failed-but-actually-succeeded requests do not double-execute.
2Race-condition defence — parallel requests with the same key serialise via the database constraint.
3Auditability — explicit operation identifier in logs. Storage: Redis with 24-hour TTL is typical; SQL table for high-trust transactions. API design: idempotency-key passed via header (Idempotency-Key per Stripe convention); server validates UUID format; server requires the key for state-changing operations. For Indian fintech / wallet / payment APIs: idempotency keys are now expected by partners; absence is a partner-integration friction. RBI inspectors increasingly look for this pattern in operational risk reviews.

Performance discipline that strengthens security

A practical performance discipline that applies regardless of the specific vulnerability class is to instrument every endpoint with three SLOs: latency at the 50th, 95th, and 99th percentiles. The 50th percentile reflects typical user experience, the 95th catches tail behaviour visible to a meaningful fraction of users, and the 99th captures the long-tail outliers that often hide bugs (timeouts on a stale dependency, locking contention under load, garbage-collection pauses on a hot endpoint). Every release should compare these percentiles against the previous baseline; regressions of more than ten percent on the 95th percentile are typically worth investigating before shipping. Modern observability stacks make this routine — Datadog APM, New Relic, Honeycomb, Grafana Tempo with OpenTelemetry instrumentation, and on the open-source side Jaeger plus Prometheus plus Grafana — but the discipline matters more than the tool. Indian product teams that have adopted this percentile-driven culture report fewer surprise outages and a smaller incident-response load over time, which compounds into a stronger security posture: the same instrumentation that catches a slow endpoint also catches an exploitation attempt that forces unusual code paths.

Observability that catches attacks alongside outages

Observability for security overlaps with observability for reliability but adds a few specific signals worth instrumenting. First, application-layer authentication and authorisation events should produce structured logs with the user identifier, the action attempted, the source IP, the User-Agent fingerprint, and the outcome. Second, every state-changing API call should log its idempotency key, the request body shape, and the resulting object identifier. Third, every external dependency call should log the destination, the response code, and the latency. Together these three categories produce a clean event stream that a SIEM can correlate to detect both functional regressions and active attacks. The cost is moderate — log storage in Elasticsearch or Splunk runs perhaps thirty to sixty rupees per gigabyte per year at typical Indian-tier prices — and the payoff is dramatic: incidents that previously took days to investigate are typically resolved in hours when the relevant evidence has been pre-indexed. Indian regulators increasingly expect this level of operational logging, particularly RBI under the Cyber Security Framework Annex 1 and SEBI under the CSCRF master circular.

DPDP and sectoral obligations a practitioner cannot ignore

Under India’s Digital Personal Data Protection Act 2023, the operational obligations on any web application that handles personal data extend beyond simple consent collection. Section 8 requires that data fiduciaries implement reasonable security safeguards — interpreted in practice as reflecting a documented information security programme, regular vulnerability assessment, and demonstrable controls aligned to a recognised standard such as ISO 27001 or NIST CSF. Section 9 imposes data principal rights including access, correction, and erasure, which means the application must be architected so that these requests can be fulfilled within the prescribed timeframes (currently expected to be on the order of thirty days). Section 8(6) imposes breach notification: when a personal-data breach occurs, the data fiduciary must notify the Data Protection Board and affected data principals within prescribed timeframes (the draft Rules suggest seventy-two hours for material breaches). For the practitioner this translates into specific engineering tasks: data inventory and lineage tracking, retention controls and automated deletion at end-of-purpose, audit-grade logging of every access to personal data, breach-detection telemetry, and a formal incident-response playbook with regulatory-notification templates. The 2024-2025 inspections by RBI and SEBI of regulated entities have already cited DPDP-overlay obligations, so the alignment is not theoretical; it shapes what auditors expect to see in the next inspection cycle.

A small threat-modelling routine before every release

A short threat-modelling exercise specific to this vulnerability class is worth running before each release. The classic STRIDE framework — Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege — provides a useful checklist when applied to the specific data flow at hand. Begin by drawing the data flow for the feature being changed: external actors at the edge, the trust boundaries they cross, the back-end services involved, the data stores read or written, and the third-party integrations called. For each trust boundary crossing, ask which STRIDE categories apply and what controls mitigate them. The output is a small set of testable assertions: this endpoint must not allow tenant A to read tenant B’s records, this mutation must require step-up authentication, this third-party call must validate the response signature, and so on. Convert each assertion into an automated test or a manual verification step in the release checklist. The discipline pays off in two ways: first, it catches design-level bugs before they reach code review; second, it produces an auditable record of security thinking that regulators value. Indian product teams that have institutionalised this practice report fewer security incidents and shorter mean-time-to-remediate when issues are found, which compounds into better customer trust and lower regulatory friction over time.

Further reading

Additional FAQs

Are race conditions limited to financial apps?

No — any state-changing operation with a check-then-act pattern is at risk. Account-deletion, password-change, MFA-disable, group-admin-add are common race targets in non-financial apps too.

Does HTTP/2 make races worse?

It dramatically reduces the network jitter that used to make races hard to reproduce. The single-packet attack with H2 lets attackers fire 100 truly-parallel requests in one packet. Your defences must assume true parallelism is achievable.

Want this for your team?

Custom team training + practitioner advisory

Beyond the free academy — we run private workshops, vCISO advisory, and red-team exercises tailored to your stack. For Indian SMBs scaling past their first hire.

Book team training call Replies in 4 working hrs · India-only · Senior consultants