What is Eventual Consistency — and When Should You Use It?
A practical introduction to eventual consistency: what it means, how it differs from strong consistency, the trade-offs involved, and how to design systems that work correctly when data is eventually consistent.
What is Eventual Consistency?
Eventual consistency is one of the most important — and most misunderstood — concepts in distributed systems. It shapes almost every architectural decision in systems that need to scale beyond a single database.
This article explains what eventual consistency is, why it exists, and how to decide when to use it.
The Problem: Distributed Data
In a single-database system, when you write a record, anyone who reads it immediately afterwards sees the updated value. The database guarantees this — it's called strong consistency.
In a distributed system, data is often stored in multiple places: replicas of a database, caches, CDN edge nodes, or separate microservices. When you write to one of these, there's a delay before the others are updated.
During that delay, different parts of your system see different versions of the data. Eventually — after the updates propagate — all nodes agree. That's eventual consistency.
A Concrete Example: DNS
You've already used eventually consistent systems. DNS is one.
When a company changes its website's IP address:
- The update is made at the authoritative DNS server
- DNS caches across the internet still hold the old IP
- Over 24–48 hours, caches expire and refresh — the new IP propagates everywhere
- Eventually, everyone resolving the domain gets the new IP
During those 48 hours, some users see the old site, some see the new one. DNS accepted this trade-off because strong consistency (every DNS server updated instantly) would require global coordination that's technically impossible at DNS scale.
Strong vs Eventual Consistency
| | Strong Consistency | Eventual Consistency | |---|---|---| | Read guarantee | Always see the latest write | May see older data | | Write cost | Must coordinate all replicas | Write to one node, propagate later | | Availability | Lower (coordination can fail or block) | Higher (write succeeds even if replicas are down) | | Latency | Higher (wait for replicas) | Lower (local write, fast) | | Complexity | System handles it | Application must handle stale reads |
Neither is better. The right choice depends on what your system is doing.
Why Accept Stale Reads?
The CAP theorem (proved by Eric Brewer in 2000) states that a distributed system can guarantee at most two of three properties simultaneously:
- Consistency — every read sees the most recent write
- Availability — every request gets a response (not an error)
- Partition tolerance — the system keeps working when nodes can't communicate
Network partitions happen in any distributed system — cables are cut, data centers lose connectivity, pods restart. You can't opt out of P. So in practice, you choose between C and A:
- CP (strong consistency): when a network partition occurs, some requests fail rather than return stale data
- AP (availability): when a partition occurs, responses continue but may be stale
Eventual consistency is the AP choice.
When Is Eventual Consistency Acceptable?
Good fit:
- Social media likes/follows — if your follower count shows 1,423 for 2 seconds before updating to 1,424, no one is harmed
- Product views and search indexes — a new product appearing in search results 5 seconds after creation is fine
- CDN content — cached web pages can be seconds or minutes old
- Analytics dashboards — whether your dashboard shows 10,000 or 10,001 current users doesn't matter for decision-making
- Read replicas — reads from a replica that's 100ms behind the primary are almost always fine
Poor fit:
- Bank account balances — you cannot allow a transfer to be based on stale balance data
- Inventory reservation — two users must not both be told the last item is available
- Authentication state — a revoked token must immediately be invalid, not eventually invalid
- Order payment — you must not charge a customer twice due to a stale "payment pending" state
The key question: what is the worst thing that happens if a user sees stale data for 1–10 seconds? If the answer is "an irritating UI glitch," eventual consistency is fine. If the answer is "we lose money or violate a contract," you need strong consistency.
Designing for Eventual Consistency
If you accept eventual consistency, your application code must account for stale reads.
Read-Your-Own-Writes
A common problem: a user submits a form, the page refreshes, and the new data isn't there yet. Frustrating and confusing.
Solution: route reads immediately after a write to the same database node (or use a short-lived cache of what the user just wrote).
// After creating a post, redirect to it using its ID
// Don't redirect to the feed — the feed read replica may not have it yet
var post = await _postService.CreateAsync(request);
return Redirect($"/posts/{post.Id}"); // direct lookup by ID against primaryMonotonic Reads
If a user refreshes a page, they shouldn't see older data than they saw before. Ensure the same user is consistently routed to the same replica session.
Optimistic Concurrency
When two users update the same record based on stale reads, use version numbers to detect conflicts:
public async Task UpdateProductPriceAsync(string productId, decimal newPrice, int expectedVersion)
{
var updated = await _db.Products
.Where(p => p.Id == productId && p.Version == expectedVersion)
.ExecuteUpdateAsync(p => p
.SetProperty(x => x.Price, newPrice)
.SetProperty(x => x.Version, expectedVersion + 1));
if (updated == 0)
throw new ConcurrencyConflictException("Product was modified by another process");
}If two updates race, one wins and the other gets a ConcurrencyConflictException — which you can handle by retrying with the fresh data.
Eventually Consistent Patterns
Outbox Pattern
The most reliable way to propagate updates to other services is the Outbox pattern: write the update and the event in the same database transaction, then publish the event asynchronously.
// In one transaction: update the order AND record the event
await using var tx = await _db.Database.BeginTransactionAsync();
order.Status = OrderStatus.Confirmed;
_db.OutboxMessages.Add(new OutboxMessage
{
Type = "OrderConfirmed",
Payload = JsonSerializer.Serialize(new OrderConfirmedEvent(order.Id)),
CreatedAt = DateTime.UtcNow
});
await _db.SaveChangesAsync();
await tx.CommitAsync();
// Separate background worker publishes outbox messages to the event bus
// If publishing fails, retry — the message is durably storedOther services consume the event and update their own state. They may be slightly behind — that's the eventual consistency. But with the Outbox, you guarantee that the event is eventually delivered (at-least-once).
CQRS Read Models
Separate your write model (strongly consistent) from your read model (eventually consistent, optimised for queries):
Write: POST /orders → OrdersDB (primary) → publishes OrderCreated event
Read: GET /dashboard → ReadDB (denormalised view, updated by event handler)The read model may be 1–2 seconds behind the write model — acceptable for dashboards. Inventory checks and payment processing still use the strongly consistent write model.
Summary
- Eventual consistency means all nodes will agree — eventually, not immediately
- It's a trade-off: higher availability and lower latency, in exchange for stale reads
- The CAP theorem forces this trade-off in any truly distributed system
- Use it where stale data causes UX issues, not business logic errors
- Design for it: read-your-own-writes, monotonic reads, optimistic concurrency
- Outbox pattern + CQRS are the practical building blocks of eventually consistent systems
Enjoyed this article?
Explore the Distributed Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.