DynamoDB GSI vs LSI
Both a Global Secondary Index (GSI) and a Local Secondary Index (LSI) let you
Query by an attribute that isn't your table's key. Which one you need depends
on which key you're changing.
What is the difference between a GSI and an LSI in DynamoDB?
A Global Secondary Index takes its own partition key and optional sort key, gets its own capacity, and can be added anytime, but serves eventually consistent reads only.
A Local Secondary Index keeps the table's partition key with a different sort key, supports strongly consistent reads and shares the table's capacity, but must be created with the table.
The differences that matter
| GSI | LSI | |
|---|---|---|
| Partition key | Any scalar (S/N/B) | Same as the table |
| Sort key | Any scalar (S/N/B), optional | Any scalar (S/N/B) |
| Key attributes | Up to 4 partition-key + 4 sort-key | Exactly one sort-key attr |
| When created | Anytime | Table-creation only |
| Consistency | Eventual only | Strong available |
| Capacity | Its own | Shares the table's |
| Write propagation | Async (eventual) | Synchronous |
| Max per table | 20 (default quota) | 5 |
| 10 GB item-collection cap | No | Yes (per partition key) |
The four-plus-four row is new: since November 2025 a GSI key can be built from several typed attributes instead of one concatenated string. An LSI's sort key is still exactly one attribute.
A rule of thumb
- Need a different partition key (e.g. look up tickets by
statusinstead ofaccount)? You need a GSI; an LSI can't repartition. - Need a second sort order within the same partition, decided up front, with strongly-consistent reads? An LSI fits: it keeps the table's exact partition key and only swaps in a different sort key.
The choice collapses to one question, which key are you changing:
Different partition key forces a GSI; a different sort key on the same partition is the only case an LSI fits.
GSIs are the usual default: they can be added later, scale on their own capacity, and have no 10 GB cap. AWS suggests a GSI whenever an item collection might grow past 10 GB. Overload a single GSI's keys to serve several patterns; see single-table design.
If you're adding a GSI to kill a Scan, remember it has its own read/write capacity. Size that extra cost with the pricing calculator.
Worked example: open tickets by priority
Say you run a support desk. Tickets live under PK = TEAM#7 with
SK = TICKET#8842. Two access patterns compete:
- Fetch one ticket by id:
GetItemon the base table key. - List open tickets for the team, highest priority first: needs a different sort order than ticket id.
Pattern 2 cannot be a base-table Query on SK alone because the sort key is the
ticket id, not priority. Your options:
| Approach | Keys | Consistency on list | When it fits |
|---|---|---|---|
GSI with GSI1PK = TEAM#7, GSI1SK = P#1#TICKET#8842, set only while open | New partition + sort | Eventual | Status changes often; add index after launch |
LSI with same PK, LSI1SK = P#1#TICKET#8842, set only while open | Same partition, new sort | Strong | Index planned at table creation; status reads must be fresh |
Filter on base Query, then sort by priority in your code | Base keys only | Strong | Tiny partitions only: the filter bills the whole partition you read |
Writing the index key only on open tickets makes both indexes
sparse: closed tickets never enter them. On the
GSI you could also make priority and ticketId two typed sort-key attributes
instead of building the P#1#TICKET#8842 string.
For a busy queue where agents refresh every few seconds, eventual consistency on a GSI is usually acceptable. For a financial ledger line that must reflect a status change before the API returns, serve the read from the base table or an LSI.
Write amplification
An update costs an index write only when it touches what that index stores: one write if it changes a projected attribute, two if it changes an index key value (delete the old entry, put the new one), none if the index doesn't store it.
Say an update changes priority, which sits in both GSI1SK and LSI1SK, on a
table that also has a GSI2 that doesn't store it. That is 2 + 2 + 0 = four index
writes plus the base write.
An index entry over 1 KB costs more than one write unit per write; the indexes overview prices this per month.
LSI writes land on the same partition as the base item, because an LSI shares its partition key. Each LSI adds its write cost to that partition, so a busy partition key reaches the per-partition ceiling sooner.
A GSI moves its writes to its own partitions, but an under-sized or hot GSI still throttles base-table writes; see why a GSI throttles base-table writes.
The 10 GB LSI item-collection cap
On a table with an LSI, AWS caps each partition-key value at 10 GB, counting
the base-table items and their LSI entries together. A table with no LSI has no
such cap, and GSIs are exempt. A tenant with millions of tickets under one
TEAM#7 can hit it.
At the cap, DynamoDB can reject writes that would grow the collection with
ItemCollectionSizeLimitExceededException; writes that shrink it still succeed.
See it coming by passing ReturnItemCollectionMetrics: "SIZE" on PutItem,
UpdateItem, DeleteItem and BatchWriteItem.
Alert when SizeEstimateRangeGB passes a threshold below 10 (AWS suggests 8 GB).
To recover, delete or trim items in that collection, or move history to a new
table or to S3. For collections that grow without bound, use a GSI from the start.
Choosing at design time
| Question | Lean GSI | Lean LSI |
|---|---|---|
| Need a different partition key? | Yes | No, LSI cannot |
| Must reads be strongly consistent? | No, GSI is eventual | Yes |
| Index added after table exists? | Yes | No, table creation only |
| Item collection may grow past ~10 GB? | Yes, no cap | Risky on LSI |
| Want isolated write scaling? | Yes | No, shares table WCU |
Already have a live table and want an LSI? You can't add one. If eventual reads
are acceptable, add a GSI with the table's partition key and the new sort key
(UpdateTable with GlobalSecondaryIndexUpdates).
If you need strong reads, CreateTable a new table with the LSI, copy the items
across (parallel Scan plus BatchWriteItem, dual-writing during the cutover),
then switch readers. Import from S3 can't help: the table it creates takes GSIs
but no LSIs.
When you overload several access patterns onto one GSI, sketch the key templates first in the single-table design tool. It shows which reads share an index and flags unsupported patterns before you pay for a backfill.
Inspect indexes before you commit
In DynoTable, run a filter against the base table, then switch the filter builder's index picker to each GSI and run it again.
Attributes missing from the GSI result are not in its projection.
Build the KeyConditionExpression for a candidate GSI in the
DynamoDB expression builder, then emit a
full paginated program with the
query builder to paste into a staging test.
Three guides that build on this choice
- GSI eventually consistent reads: why a just-written item can be missing from a GSI query for a moment.
- Index projections: what each index physically stores.
- Sparse indexes: keep GSIs small by indexing only items that carry a marker attribute.
Try DynoTable to inspect each index's keys and projection on your own tables before you commit to a design.