DEV Community

Cover image for Why a DynamoDB GSI Is Eventually Consistent
DynoTable
DynoTable

Posted on • Edited on • Originally published at dynotable.com

Why a DynamoDB GSI Is Eventually Consistent

You write an item, immediately query a Global Secondary Index for it, and get
nothing back — even though the write succeeded and a base-table GetItem
returns the item fine.

Nothing is broken. You've hit the most surprising property of GSIs: every read
of a GSI is eventually consistent
. There's a brief window after a write where
the index hasn't caught up yet.

Are DynamoDB GSIs eventually consistent?

Yes — every read of a Global Secondary Index is eventually consistent, with no
way to opt out. Your write commits to the base table first, then propagates
asynchronously to the index, so a query issued right after a write can
return stale or missing rows. DynamoDB offers no ConsistentRead flag for a GSI.

  • A GSI is a separate, asynchronously replicated table — your write commits to the base table first, then propagates to the index.
  • No ConsistentRead flag exists for a GSI. Unlike the base table, you can't force a strong read to close the gap.
  • Read-your-own-writes from the base table, not the GSI. You already hold the primary key right after a write.
  • Enforce uniqueness with a conditional write, not a GSI query. The propagation gap turns a "is this taken?" check into a race.

The symptom: a sign-up that "can't find itself"

Take a Members table for a user-accounts service. The base table is keyed by an
internal id, but users log in by email, so there's an email-lookup GSI:

Members (base table)

PK SK email displayName
ACC#a1f9c PROFILE ada@northwind.test Ada L.

EmailIndex (GSI)

GSI1PK GSI1SK
ada@northwind.test ACC#a1f9c

The sign-up flow does two things back to back: PutItem the new member, then
Query EmailIndex WHERE GSI1PK = "ada@northwind.test" to check no one else
claimed that address and to load the profile.

Run those two calls a few milliseconds apart and the Query can return zero
items
. Do it again a second later and the row is there. The write didn't fail
— the index just hadn't been updated yet.

Why this happens: GSIs are replicated asynchronously

A GSI is a separate, internally managed table with its own partitions and its
own key schema. It is not maintained inside the same transaction as your
base-table write.

When you PutItem, DynamoDB durably commits to the base table, acknowledges your
write, and then asynchronously propagates the change to each GSI. The AWS
GSI documentation
states it plainly: GSIs support eventually consistent reads only.

The propagation delay between a base-table write and the index update is usually a
fraction of a second — but it is not guaranteed and not bounded under load.
Designing as if it were bounded is the trap.

This is not a bug; it's the original Dynamo design trade-off. The 2007
Amazon Dynamo paper
chose availability and partition tolerance over strong consistency.

GSIs inherit that lineage. Loose coupling is what lets the index scale and stay
writable independently of the base table.

Why a DynamoDB GSI Is Eventually Consistent

The gap between the 200 OK and "replicate change" is the window where your index
read is stale. There is no consistent-read flag that closes it.

Unlike the base table — where you pass ConsistentRead = true to force a
strongly consistent GetItem/Query — a GSI flatly rejects that option.

An LSI can be read strongly because it shares the base table's partitions; see
GSI vs LSI for why that distinction exists.

A subtler trap: stale old values, not just missing new ones

The missing-row case is the obvious one. The quieter bug is reading a stale
previous value
.

Say Ada changes her email from ada@northwind.test to ada.l@northwind.test. The
base table updates atomically, but for a moment the GSI can still return the
old index entry.

A lookup against the new value misses, while the abandoned value still resolves.

Worse: if you query the GSI and write back based on what you read, you can act on a
value that no longer exists. Treat any GSI read as a snapshot that may lag reality.

Design around it — don't fight it

The propagation window is real, so the fix is architectural, not a retry knob you
toggle. Four patterns, roughly in order of preference:

  1. Read your own writes from the base table. Right after a write you already hold the primary key (ACC#a1f9c), so do a strongly consistent GetItem on the base table instead of querying the GSI.

The GSI is for the other access pattern — "I have an email, find the account"
— not for confirming the write you just made.

  1. Enforce uniqueness with a guard item, not the GSI. Never trust a GSI query to prove an email is unclaimed — the propagation gap makes that a race two simultaneous sign-ups can both lose.

Instead, write a dedicated uniqueness item keyed on the email itself
(PK = "EMAIL#ada@northwind.test") inside a TransactWriteItems with a
ConditionExpression of attribute_not_exists(PK).

Strongly consistent base-table conditions, applied atomically, are what actually
enforce uniqueness.

   TransactWriteItems:
     - Put member item    (PK = ACC#a1f9c, SK = PROFILE)
     - Put uniqueness item (PK = EMAIL#ada@northwind.test)
         ConditionExpression: attribute_not_exists(PK)
Enter fullscreen mode Exit fullscreen mode

If a second sign-up races for the same address, its condition fails and the
whole transaction is rejected — no GSI, no propagation delay, no double-claim.

Build and preview that attribute_not_exists condition with the
DynamoDB Expression Builder
before you wire it into code.

  1. Tolerate the lag in the UX. When the GSI read genuinely is the right tool (login by email for an existing user), the window is sub-second and harmless — an established account propagated long ago.

Reserve the strongly consistent base-table path for the read-after-write moment
only.

  1. Re-query, don't assume. If a workflow must observe a brand-new item through the GSI, treat an empty result as "not yet visible," not "does not exist," and re-query after a short backoff.

But prefer patterns 1 and 2, which remove the guesswork entirely.

See the propagation gap yourself

The fastest way to build intuition is to watch it happen. In DynoTable you put an
item into the base table and immediately query the GSI in a second tab.

On a loaded table you'll occasionally catch the index trailing the base data, then
watch it converge on the next refresh.

Seeing the lag with your own data makes the "read your own writes from the base
table" rule stick far better than any diagram.

Pitfalls and next steps

  • Don't gate logic on a GSI read-after-write. Uniqueness checks, "did my write land" confirmations, and read-modify-write loops belong on the strongly consistent base table.
  • Don't reach for ConsistentRead on a GSI — it isn't allowed and will error.
  • Don't model an access pattern as a GSI when the base key already answers it. Serve a read from the primary key and you skip the propagation window entirely.

Picking the right key shape is the whole game in
single-table design; knowing when a Query beats a
Scan keeps you off the index in the first place
(Query vs Scan).

Build and test your uniqueness ConditionExpression in the
DynamoDB Expression Builder. Then
try DynoTable to watch base-table writes propagate to a GSI in real
time, and design your keys so the eventual-consistency window never bites you.

Top comments (0)