A race condition occurs when the outcome of a computation depends on the unpredictable timing of multiple threads or processes accessing shared resources.

Let’s see this in action. Imagine two processes, A and B, trying to update a counter stored in a shared database.

Process A:

  1. Reads the current value of the counter (e.g., 10).
  2. Increments it in memory (to 11).
  3. Writes the new value back to the database.

Process B:

  1. Reads the current value of the counter (e.g., 10).
  2. Increments it in memory (to 11).
  3. Writes the new value back to the database.

If these operations interleave perfectly, the final value in the database will be 11, even though two increments were attempted. The second write overwrites the first.

In distributed systems, this problem is amplified by network latency and the inherent lack of a single, global clock. We can’t rely on simple mutexes or semaphores that work within a single process. Instead, we need mechanisms that coordinate across multiple machines.

The core problem is that operations are not atomic. A read-modify-write sequence on a shared resource (like a database row, a cache entry, or a file) can be interrupted by another process.

Common Causes and Fixes for Race Conditions:

  1. Uncoordinated Updates to Shared Data: This is the classic scenario. Multiple clients or services independently read data, modify it, and write it back without any form of locking or versioning.

    • Diagnosis: Observe inconsistent data states after concurrent operations. For example, two users booking the last seat on a flight, but both succeeding.
    • Fix: Implement optimistic concurrency control using version numbers or timestamps.
      • Example (Database): Add a version column to your table. When updating, include WHERE id = ? AND version = ? and increment the version in the SET clause. If the WHERE clause matches zero rows, it means another process updated it first.
      • Why it works: Each update attempt includes the expected version. If the version has changed (meaning another update occurred), the WHERE clause fails, preventing the overwrite and signaling a conflict.
    • Diagnosis Command/Check: SELECT * FROM bookings WHERE flight_id = 123; (look for multiple concurrent bookings with the same version value).
    • Fix Command: UPDATE bookings SET status = 'confirmed', version = version + 1 WHERE id = ? AND version = ?;
  2. Read-Your-Own-Writes Inconsistency: A client reads data, writes an update, but then subsequent reads might return stale data from a replica that hasn’t yet received the write.

    • Diagnosis: A user updates their profile, but then immediately sees the old information on a different page or after a refresh.
    • Fix: Use session affinity or "read-your-own-writes" consistency guarantees.
      • Example (API Gateway/Load Balancer): Configure sticky sessions so that a client’s requests are always routed to the same server instance.
      • Example (Database/Cache): When performing a read after a write, explicitly direct the read to the primary/master instance, or use a read-after-write consistency setting if available (e.g., Redis READONLY command followed by a GET to the master).
      • Why it works: By ensuring reads go to the most up-to-date data source or the same source that processed the write, you guarantee that the client sees their own changes immediately.
    • Diagnosis Command/Check: Network trace showing requests hitting different backend instances after a write.
    • Fix Config: Load balancer configuration for sticky_sessions = true or session_cookie_path = /.
  3. Distributed Locks with Timeouts: Relying on distributed locks (e.g., using ZooKeeper, etcd, or Redis SETNX) without proper timeout handling can lead to deadlocks or orphaned resources. If a process acquires a lock and then crashes, the resource remains locked indefinitely.

    • Diagnosis: Operations that should be exclusive are not, or services hang indefinitely waiting for a lock.
    • Fix: Implement leases or fencing tokens for distributed locks.
      • Example (etcd): Use etcd’s lease mechanism. Acquire a lock associated with a lease. If the client fails to renew the lease, etcd automatically revokes it, releasing the lock.
      • Why it works: The lock is automatically released after a predefined period if the client holding it fails to signal its continued existence, preventing indefinite blocking.
    • Diagnosis Command/Check: etcdctl lease grant 60 (observe lease expiration) or etcdctl alarm list for potential issues.
    • Fix Command: etcdctl lock /my/lock --ttl 60 (this implicitly uses a lease).
  4. Message Queue Ordering Issues: In systems using message queues for distributed coordination, if messages aren’t processed in the exact order they were sent, race conditions can occur. For example, an "order updated" message arriving before an "order created" message.

    • Diagnosis: Application logic fails because events are processed out of sequence.
    • Fix: Use ordered message queues or implement sequence numbers within messages.
      • Example (Kafka): Ensure messages are sent to the same partition for a given key (e.g., order_id). Kafka guarantees order within a partition.
      • Example (RabbitMQ): Use publisher confirms and careful consumer logic that buffers messages until dependencies are met.
      • Why it works: Guaranteeing order of operations on a per-entity basis (like an order) ensures that state transitions happen logically.
    • Diagnosis Command/Check: kafka-console-consumer --topic orders --partition 0 --from-beginning (observe message order).
    • Fix Config: Kafka producer partitioner.class=org.apache.kafka.clients.producer.internals.DefaultPartitioner (default, but ensure the key is consistent).
  5. Cache Invalidation Race Conditions: When invalidating cached data, a race can occur between the invalidation signal and a read operation that fetches stale data before the invalidation is processed.

    • Diagnosis: Users see outdated information that should have been cleared from the cache.
    • Fix: Use a "write-through" cache or a two-phase invalidation.
      • Example (Write-through): Always write updates to the cache and the database simultaneously. Reads always go to the cache.
      • Example (Two-phase): First, mark the item as "stale" in the cache. Then, asynchronously trigger the actual deletion or update. Subsequent reads that hit the "stale" item will fetch fresh data from the source and update the cache.
      • Why it works: The stale state is temporary and immediately corrected by fetching fresh data, preventing a window where truly old data is served.
    • Diagnosis Command/Check: redis-cli GET my_key followed by redis-cli GET my_key after an update to see if it changes.
    • Fix Config: Cache client configuration for writeThrough: true or implementing a custom invalidation handler.
  6. Clock Skew in Timestamp-Based Coordination: If systems rely on timestamps for ordering or locking and their clocks are significantly out of sync, it can lead to incorrect ordering and race conditions.

    • Diagnosis: Operations appear to happen out of order, or locks are granted incorrectly based on time.
    • Fix: Use Network Time Protocol (NTP) to synchronize clocks across all nodes, or preferably, use logical clocks (like Lamport timestamps or Vector clocks) that don’t rely on physical time.
      • Why it works: NTP ensures physical clocks are close enough for practical purposes. Logical clocks provide a guaranteed ordering of events that is independent of physical time, preventing paradoxes.
    • Diagnosis Command/Check: ntpq -p on multiple servers to check NTP synchronization status.
    • Fix Command: Ensure NTP daemon is running and configured correctly on all nodes.

The next error you’ll encounter if you’ve fixed race conditions but haven’t addressed eventual consistency is a "Stale Read" error, where your system might still serve data that is not the absolute latest version.

Want structured learning?

Take the full Distributed Systems course →