← All insights

Handling Concurrent Bids in a Real-Time Auction System

The hard part of a live auction is not accepting a bid when only one person is bidding. It is deciding what happens when several requests target the same auction at nearly the same moment—and making that decision durable, explainable and safe to retry.

A bidder sees a countdown and a current price. Behind that interface, the system has to enforce the auction's rules even when requests arrive on different API instances, database transactions overlap, a browser retries after a timeout, or the closing deadline passes while work is in progress.

This article focuses on the critical path: how to choose one authoritative state transition for each auction, preserve accepted bids, and distribute updates without allowing a cache, WebSocket message or asynchronous consumer to decide the winner.

1. Define the invariant before choosing the lock

Suppose the current accepted bid is ₹9,000. Bidder A submits ₹10,000 while Bidder B submits ₹10,500. Both requests read ₹9,000 before either writes. Each independently passes a minimum-increment check.

If the implementation then performs an unprotected read-modify-write, the final current amount can reflect whichever write happens last, while the history may contain conflicting decisions. A more subtle defect is returning “accepted” to both bidders even though one update has overwritten the other.

Concurrent bid flow showing competing requests entering one auction-specific serialization boundary before the durable state change is announced.

Start by stating the invariants in business terms:

  • Every accepted bid belongs to an identified auction and eligible bidder.
  • The auction's status and bid rules are checked against authoritative state.
  • Conflicting changes to one auction are ordered by a documented server-side rule.
  • A successful response corresponds to a durable decision, not merely a successful cache write.
  • Retrying the same logical request does not create a second bid.
  • A closed auction cannot accept a bid outside its published closing policy.

These invariants are more durable than a particular technology choice. They can be implemented with database locks, optimistic concurrency or a carefully partitioned per-auction processor. The mechanism must preserve the same contract.

2. Put the decision inside a short transaction

For a relational design, a common approach is to lock the auction row, validate the bid against the latest state, write the bid and update the auction in one transaction. The exact schema and isolation behavior need to be tested with the database engine and indexes in use.

A simplified MySQL-style flow looks like this:

START TRANSACTION;

SELECT status, current_bid, version
FROM auctions
WHERE id = ?
FOR UPDATE;

-- Validate auction status, server-side closing policy,
-- bidder eligibility, amount and minimum increment.

INSERT INTO bids (
  auction_id, bidder_id, amount, idempotency_key, accepted_at
) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP(3));

UPDATE auctions
SET current_bid = ?, version = version + 1
WHERE id = ?;

-- Insert a durable outbox event in the same transaction.
INSERT INTO outbox_events (
  event_id, aggregate_id, event_type, payload, created_at
) VALUES (?, ?, 'bid.accepted', ?, CURRENT_TIMESTAMP(3));

COMMIT;

This is illustrative, not a drop-in schema. A real implementation must handle rejected bids, unique constraints, database errors, currency precision, audit fields and transaction rollback. The application should return “accepted” only after the commit succeeds.

With a row-level lock, requests for the same auction serialize at that row, while requests for different auction rows can often progress independently. The lock scope should be no broader than necessary: holding a transaction open while calling an email provider, waiting for a WebSocket acknowledgement or performing unrelated work increases contention.

The important measurement is the time spent inside the serialized critical section—not the total time a user waits for the page. Authentication, network transfer, unrelated queries and external calls should normally sit outside that lock.

3. Choose pessimistic or optimistic concurrency deliberately

There is no single locking strategy that fits every auction.

ApproachUseful whenMain trade-off
Row lock and transactionThe state transition is naturally represented by one auction record and correctness is paramountConflicting bids wait; long transactions can create a queue
Optimistic version checkConflicts are relatively infrequent and retries are affordableCompeting updates fail their version check and must be re-evaluated
Per-auction serialized worker or partitionA small number of hot auctions create concentrated contentionOrdering, failover, backpressure and durable recovery need careful design

With optimistic concurrency, a write can include the version that was read:

UPDATE auctions
SET current_bid = ?, version = version + 1
WHERE id = ? AND version = ?;

If zero rows are updated, another request changed the record first. Reload the state, re-apply the business rules and retry only under a bounded policy. Do not blindly replay the old decision against the new state.

A high-throughput system may route work by auction ID so that one auction's decisions are processed in a defined order. This can reduce lock competition, but it does not remove the need for durable state, idempotent requests, recovery after worker failure and a safe closing transition.

Redis can help with selected reads or coordination, but a lease or cache entry alone should not become the sole evidence that a bid was accepted. Lease expiry, pauses and network partitions can complicate lock ownership. Keep the business outcome recoverable from durable records.

4. Make client retries idempotent

A timeout does not prove that the server failed. The transaction may have committed while the response was lost. If the browser submits the same bid again as a new operation, the user can accidentally create a second bid or receive a misleading rejection.

Give each logical bid request an idempotency key. Store the key with the resulting decision and enforce uniqueness in durable storage—for example, with a unique constraint scoped to the auction and bidder. If a repeated request arrives with the same key and the same request payload, return the recorded result. If the key is reused with a different payload, reject it rather than silently treating the requests as equivalent.

The request's result should be persisted alongside the business operation so the server can recover the original response after a restart. An in-memory cache may reduce lookup cost, but it is not a substitute for durable deduplication.

Retries also need limits. A conflict retry should reload and revalidate the auction; it should not reuse a stale “valid” decision. Under sustained contention, uncontrolled retries amplify load precisely when the system is already struggling. Use bounded retries, backoff where appropriate, and an explicit response when the client should refresh state or try again.

5. Treat closing as a competing state transition

The auction deadline creates another race. A bid can reach the API just before the displayed countdown reaches zero but wait behind a lock. Meanwhile, a scheduler may try to close the auction.

Define the policy before implementation:

  • Is eligibility based on server receipt time, a database timestamp, or the serialized order in which the bid is committed?
  • Does a bid received before the deadline remain eligible if processing finishes after it?
  • Are there anti-sniping extensions, and which accepted events can trigger them?
  • How are queued bids handled when closure begins?
  • Which transition determines the final winner and prevents any later bid from changing it?

There is no universal answer for every auction model. The policy must be published to bidders, encoded in server-side state transitions and tested at the deadline boundary. A client clock is informational only.

Closing should compete safely with bid processing through the same authoritative state model. If the close operation changes the auction from Open to Closed, the bid transaction must check that state as part of its decision. A delayed scheduler, duplicated close job or retried bid must not create a second winner determination.

6. Commit the decision before broadcasting it

After a bid is accepted, the platform may need to update connected bidders, send notifications, write analytics and inform other services. These are reactions to the decision; they should not determine whether the bid is valid.

A common failure window occurs when the database commit succeeds but publishing bid.accepted to Kafka fails. The accepted bid is durable, yet downstream consumers never learn about it. Publishing first creates the opposite risk: consumers see an event for a transaction that later rolls back.

For business-critical events, a transactional outbox can close this gap. Write the auction update and an outbox record in the same database transaction. A separate publisher sends pending records to Kafka and marks them delivered according to a retryable process. Consumers should be idempotent because delivery may be repeated.

Key the event by auction ID when per-auction ordering is required, and include a stable event ID, aggregate version and correlation ID. Kafka preserves order within a partition, not globally across a topic. The application should broadcast a committed state change through the real-time layer, while reconnecting clients reload an authoritative snapshot instead of trusting every WebSocket message to have arrived.

For a deeper look at delivery guarantees, retries and consumer recovery, see the Kafka event-driven notification architecture guide.

7. Estimate contention, then test the actual workload

Consider a platform receiving 72 bids per second, with 25% directed at its busiest auction. A short burst doubles the rate to that auction, and the serialized critical section averages 25 milliseconds.

  • Modeled arrival rate to the hot auction: 72 × 25% × 2 = 36 bids/second.
  • Approximate serialized service capacity: 1,000 ÷ 25 = 40 bids/second.
  • Modeled utilization: 36 ÷ 40 = 90%.

At 90% utilization, a simple M/M/1 queue model estimates roughly 225 milliseconds of mean queue wait and 250 milliseconds in the queue plus serialized service. These figures are a mathematical illustration, not a production benchmark or a latency guarantee. Real auction traffic may arrive in synchronized bursts, and database lock behavior may not follow the model's assumptions.

Use the Auction Contention Estimator to change the arrival rate, hot-auction share, burst multiplier and critical-section duration. It reports when the modeled arrival rate reaches or exceeds serialized capacity instead of presenting a misleading finite queue estimate.

For broader request-rate and bandwidth assumptions, compare the System Design Capacity Calculator with the network infrastructure capacity planning guide. Platform-wide capacity is useful, but it can hide a single hot auction that is the true bottleneck.

8. Instrument the invariant, not just the API

Request latency alone will not tell the team why an auction is slow. Capture measurements that connect the bid request to the durable state transition and subsequent event delivery.

Useful signals include:

  • Accepted and rejected bid counts, with rejection reasons.
  • Lock wait time and transaction duration, including p95 and p99.
  • Optimistic conflict and retry counts.
  • Duplicate idempotency-key requests and payload mismatches.
  • Queue depth and age for any per-auction worker.
  • Outbox backlog, Kafka consumer lag and real-time delivery delay.
  • Closing-transition delay and bids rejected at the deadline boundary.

Break down contention by auction or workload class without putting bidder secrets or unnecessary personal data into logs. A healthy platform-wide average can conceal one popular auction whose queue is growing.

Test simultaneous requests against the same auction, concurrent bids against different auctions, repeated requests after a simulated timeout, a crash after commit but before event publication, database rollback, consumer redelivery, and bid-versus-close races. Include realistic hot-key distributions rather than distributing all load evenly across many auctions.

Before a production rollout, validate the design under representative load and failure conditions. Capacity estimates are useful for choosing the test; they are not a substitute for it. For release and operational policy, connect the observed latency and error signals to the SLO and error-budget planning guide and its SLO Error Budget Calculator.

Closing perspective

Correct concurrent bidding is a state-transition problem first and a scaling problem second. Choose a clear serialization boundary, keep the critical transaction short, make retries idempotent, coordinate closure with bid acceptance, and publish downstream events only after the authoritative decision is durable.

Then measure the hot path. If contention is concentrated in a few auctions, scale and partition around that reality rather than assuming that more API replicas will fix a serialized database record.

The same production discipline applies outside the auction engine: prepare safe infrastructure changes with the AKS production-readiness guide, and protect the public API boundary using the Cloudflare and Nginx origin-security guide.

Related reading