DynamoDB GSI Cost: Why One Write Can Cost 7 Units, the 100-Byte Storage Overhead, KEYS_ONLY vs INCLUDE vs ALL, Sparse Indexes, GSI vs LSI, and a Calculator

Logeshwaran
—

A DynamoDB global secondary index (GSI) costs you writes, storage and reads, separately from its table. Every time you write an item that belongs in an index, DynamoDB writes the index too, and you pay for that write: at on-demand prices in US East, $0.625 per million write request units on the table and again on each index, plus $0.25 per GB-month for the copy the index stores. Here is the number that surprises people. In our test, one 2.5 KB write into a table with two indexes cost 7 write units, not 3: 3 for the table, 3 for an index that projects all attributes, and 1 for a keys-only index. And an update that changes an index key costs 2 writes on that index, one to delete the old entry and one to add the new one. To cut the bill, project only the attributes your queries return, keep rarely queried items out of the index (a sparse index), and avoid indexing attributes that change all the time. This guide shows the rules, our DynamoDB Local test, a calculator and every way to pay less.

Jake's repair-orders table had two indexes that Ethan added in a hurry: one to find orders by customer email and one to list orders by status. The DynamoDB line on the bill went up more than the order count did. "We didn't add more orders," Jake said. "Why did the database get twice as expensive?" "Because every order is now written three times," Ethan said. "And some of them four."

⚡ Quick Answer

• Writes: each write that adds, changes or removes an item's index entry costs index write units (1 per KB of the index entry); changing the index key costs 2. The rules.

• Storage: each index item = table key + index key + projected attributes + 100 bytes, at $0.25 per GB-month. Storage.

• Reads: index queries use the index's capacity and are eventually consistent only. Reads.

• Biggest saving: project KEYS_ONLY or INCLUDE instead of ALL; in our example it cut the monthly bill from $168.25 to $95.12. Calculator.

• Provisioned tables: an index without enough write capacity throttles writes to the table. Provisioned mode.

Our test · Projections · Sparse indexes · Adding a GSI · GSI vs LSI · See the cost · FAQ

New to DynamoDB? It is AWS's serverless key-value database; our plain-English DynamoDB guide covers tables, keys and items. A table can be read efficiently only by its own key. A global secondary index gives you a second key to query by, such as an email address or a status, by keeping a second copy of (some of) each item, organized by that key. DynamoDB maintains that copy for you, which is convenient and is also exactly why it costs money. Prices on this page are US East (N. Virginia) on October 24, 2026.

🧭 NEW HERE? READ THESE FIRST

New to DynamoDB pricing? Start with these five pages:

💡 Before you add an index, write down the one query it is for and the attributes that query returns. That single line decides most of what the index will cost.

How a DynamoDB GSI is billed

An index is billed on the same three meters as a table, and the index's usage is added to the table's:

MeterOn-demand (Standard)Provisioned (Standard)When the index uses it
Writes$0.625 per million write request units$0.00065 per WCU-hourEvery table write that changes the item's index entry
Reads$0.125 per million read request units$0.00013 per RCU-hourEvery Query or Scan on the index
Storage$0.25 per GB-month (first 25 GB-months free, shared with the table)Every item in the index, always

The Standard-Infrequent Access table class uses different rates ($0.78 per million writes, $0.155 per million reads and $0.10 per GB-month on demand), and an index always uses the same table class as its table. A table in on-demand mode has on-demand indexes; a provisioned table needs capacity set for each index. Our on-demand vs provisioned guide explains the two modes.

"So an index is a second table I didn't create," Jake said. "A second table you never write to yourself," Ethan said, "but you pay for every write DynamoDB makes to it."

The GSI write rules: when an index write happens, and how many

Whether a table write also writes an index depends on what happens to the item's entry in that index:

What the table write doesIndex writes
Adds a new item that has the index key, or sets the index key on an item that did not have it1 (put into the index)
Changes the value of the index key (for example status open to closed)2 (delete the old entry, put the new one)
Removes the index key attribute, or deletes the item1 (delete from the index)
Changes only attributes projected into the index1 (update the projection)
Changes only attributes that are neither the index key nor projected0
The item is not in the index before or after the write0

Each of those writes costs one write unit per 1 KB of the index entry, rounded up. The index entry is not the whole item; it is the table's key, the index's key and the projected attributes. That is why projection matters so much: an index that projects all attributes has entries as big as the item, so a 2.5 KB item costs 3 write units in the index, while a keys-only index entry stays under 1 KB and costs 1.

Put together, the cost of one table write is the table's write units plus the index write units for every index the write touches. With five indexes, a write that touches all of them costs six times what the same write costs on a table without indexes, and more if it changes index keys.

Our test: measuring GSI write costs in DynamoDB Local

We built a table in DynamoDB Local 3.3.1, AWS's downloadable version of DynamoDB, running in a container capped at 768 MB of memory. The table has a simple key (pk) and two indexes: by-status (projection KEYS_ONLY) and by-email (projection ALL). Each write asked for ReturnConsumedCapacity="INDEXES", which reports the capacity used by the table and by each index separately:

1 put, both index keys present (small item)        total=3.0 table=1.0 indexes={'by-status': 1.0, 'by-email': 1.0}
2 put, no index keys (sparse: in neither index)    total=1.0 table=1.0 indexes={}
3 update a non-key attribute (note)                total=2.0 table=1.0 indexes={'by-email': 1.0}
4 update the by-status key (open -> closed)        total=4.0 table=1.0 indexes={'by-status': 2.0, 'by-email': 1.0}
5 remove the email attribute (leaves by-email)     total=2.0 table=1.0 indexes={'by-email': 1.0}
6 put a 2.5 KB item with both index keys           total=7.0 table=3.0 indexes={'by-status': 1.0, 'by-email': 3.0}
7 delete the 2.5 KB item                           total=7.0 table=3.0 indexes={'by-status': 1.0, 'by-email': 3.0}

Every line follows the rules above:

  • Line 2 is a sparse write: the item has neither index key, so only the table pays.
  • Line 3 changes a note. The keys-only index does not store notes, so it costs nothing; the ALL index stores everything, so it pays 1.
  • Line 4 changes the status. The status index pays 2 (delete and put), and the ALL index pays 1 because the status is one of its projected attributes.
  • Lines 6 and 7 are the expensive ones: a 2.5 KB item costs 3 on the table and 3 on the ALL index, plus 1 on the keys-only index. Seven units for one write, and seven again to delete it.

We also asked for a strongly consistent query on an index, and DynamoDB refused: ValidationException: Consistent reads are not supported on global secondary indexes. The full script is below, so you can run the same test against DynamoDB Local or, with --real, against a test table in your own account. Our DynamoDB Local guide shows how to start the container.

"""GSI write-cost lab against DynamoDB Local (or real DynamoDB with --real)."""
import sys, boto3
real = '--real' in sys.argv
kw = {} if real else dict(endpoint_url='http://127.0.0.1:8000', aws_access_key_id='x', aws_secret_access_key='x')
ddb = boto3.client('dynamodb', region_name='us-east-1', **kw)
T = 'gsi-cost-lab'
try: ddb.delete_table(TableName=T); ddb.get_waiter('table_not_exists').wait(TableName=T)
except ddb.exceptions.ResourceNotFoundException: pass
ddb.create_table(TableName=T, BillingMode='PAY_PER_REQUEST',
    AttributeDefinitions=[{'AttributeName': n, 'AttributeType': 'S'} for n in ('pk', 'status', 'email')],
    KeySchema=[{'AttributeName': 'pk', 'KeyType': 'HASH'}],
    GlobalSecondaryIndexes=[
        {'IndexName': 'by-status', 'KeySchema': [{'AttributeName': 'status', 'KeyType': 'HASH'}], 'Projection': {'ProjectionType': 'KEYS_ONLY'}},
        {'IndexName': 'by-email', 'KeySchema': [{'AttributeName': 'email', 'KeyType': 'HASH'}], 'Projection': {'ProjectionType': 'ALL'}}])
ddb.get_waiter('table_exists').wait(TableName=T)

def show(label, resp):
    cc = resp.get('ConsumedCapacity', {})
    idx = {k: v.get('CapacityUnits') for k, v in (cc.get('GlobalSecondaryIndexes') or {}).items()}
    print(f"{label:50} total={cc.get('CapacityUnits')} table={cc.get('Table', {}).get('CapacityUnits')} indexes={idx}")

S = lambda v: {'S': v}
R = dict(ReturnConsumedCapacity='INDEXES')
show('1 put, both index keys present (small item)', ddb.put_item(TableName=T, Item={'pk': S('a'), 'status': S('open'), 'email': S('a@x.com'), 'note': S('hi')}, **R))
show('2 put, no index keys (sparse: in neither index)', ddb.put_item(TableName=T, Item={'pk': S('b'), 'note': S('hi')}, **R))
show('3 update a non-key attribute (note)', ddb.update_item(TableName=T, Key={'pk': S('a')}, UpdateExpression='SET note = :n', ExpressionAttributeValues={':n': S('hello')}, **R))
show('4 update the by-status key (open -> closed)', ddb.update_item(TableName=T, Key={'pk': S('a')}, UpdateExpression='SET #s = :s', ExpressionAttributeNames={'#s': 'status'}, ExpressionAttributeValues={':s': S('closed')}, **R))
show('5 remove the email attribute (leaves by-email)', ddb.update_item(TableName=T, Key={'pk': S('a')}, UpdateExpression='REMOVE email', **R))
show('6 put a 2.5 KB item with both index keys', ddb.put_item(TableName=T, Item={'pk': S('c'), 'status': S('open'), 'email': S('c@x.com'), 'note': S('x' * 2500)}, **R))
show('7 delete the 2.5 KB item', ddb.delete_item(TableName=T, Key={'pk': S('c')}, **R))
try:   # GSIs only support eventually consistent reads
    ddb.query(TableName=T, IndexName='by-status', KeyConditionExpression='#s = :s', ExpressionAttributeNames={'#s': 'status'},
              ExpressionAttributeValues={':s': S('closed')}, ConsistentRead=True)
except Exception as e: print('8 strongly consistent query on a GSI ->', type(e).__name__, str(e)[:160])
ddb.delete_table(TableName=T)

"Seven," Jake said, looking at line 6. "For one order." "For one big order with an index that copies everything," Ethan said. "That's the line we fix first."

GSI storage cost: the 100-byte overhead

Each item in an index takes up:

  • the size of the table's primary key (partition key and sort key), plus
  • the size of the index key attributes, plus
  • the size of the projected attributes, plus
  • 100 bytes of overhead per index item.

Items without the index key are not stored in the index at all. For a keys-only index on a table of small items, the 100-byte overhead is often bigger than the data itself: an index entry with 40 bytes of keys takes 140 bytes, so 100 million items cost about 14 GB, or $3.50 a month. For an ALL index on 2.5 KB items, every entry is about 2.64 KB, so the same 100 million items add 264 GB, $66 a month, which is more than the table's own storage costs after the free 25 GB. Storage is the cost that never stops: you pay for it every month whether anyone queries the index or not.

GSI read costs: eventually consistent only

Queries and scans on an index use the index's read capacity, not the table's, and they are always eventually consistent. That makes them cheap: an eventually consistent read costs half a read unit per 4 KB, so one read unit returns up to 8 KB of index entries. Sizes are counted from the index entries, so a keys-only or narrow INCLUDE index is cheaper to read as well as to write.

Two read traps cost more than people expect:

  • Fetching the rest of the item. A GSI query returns only the projected attributes. If your code then calls GetItem on the table for each result to fetch the other attributes, you pay for those table reads too. A GSI never fetches from the table for you. For one query that runs constantly, projecting the attributes it needs can be cheaper than all those follow-up reads; for a rare query, it is cheaper to fetch.
  • Reading too soon after a write. Index updates are asynchronous, so a query right after a write may not see it yet. Code that "fixes" this by retrying in a loop pays for every retry. If you need read-after-write, read the table by its key.

Large index queries return at most 1 MB per call; our pagination guide shows how to read the rest with LastEvaluatedKey.

A GSI cost calculator, with a worked example

This script estimates the monthly on-demand cost of a table and its indexes. For each index, you give the size of its entries, how many items are in it, how many of your writes touch it and how many change its key. It applies the rules above and US East prices:

#!/usr/bin/env python3
"""gsi_cost_calc.py: monthly on-demand DynamoDB write + storage cost for a table and its GSIs (US East, Oct 2026)."""
import math
WRU, STORAGE, FREE_GB = 0.625 / 1e6, 0.25, 25          # $ per write request unit, $ per GB-month, free GB-months

def wru(size_bytes): return max(1, math.ceil(size_bytes / 1024))

def monthly(writes, item_bytes, items, indexes):
    out = dict(table_wru=writes * wru(item_bytes), index_wru=0, base_gb=items * item_bytes / 1e9, index_gb=0)
    for ix in indexes:
        entry = ix['key_bytes'] + ix['projected_bytes'] + 100
        out['index_wru'] += (ix['writes_touching'] + ix['key_changes']) * wru(entry)
        out['index_gb'] += ix['items_in_index'] * entry / 1e9
    out['write_cost'] = (out['table_wru'] + out['index_wru']) * WRU
    out['storage_cost'] = max(0, out['base_gb'] + out['index_gb'] - FREE_GB) * STORAGE
    out['total'] = out['write_cost'] + out['storage_cost']
    return out

if __name__ == '__main__':
    # 100 million orders of 2.5 KB; 10 million writes a month =
    # 2 million new orders + 3 million status changes + 5 million other updates
    r = monthly(writes=10_000_000, item_bytes=2500, items=100_000_000, indexes=[
        dict(name='by-email (ALL)', key_bytes=40, projected_bytes=2500, items_in_index=100_000_000,
             writes_touching=10_000_000, key_changes=0),
        dict(name='by-status (KEYS_ONLY)', key_bytes=40, projected_bytes=0, items_in_index=100_000_000,
             writes_touching=5_000_000, key_changes=3_000_000)])
    for k, v in r.items(): print(f'{k:13} {v:,.2f}')

We checked it against the DynamoDB Local test: for single writes it gives 3, 2, 4 and 7 units for the same four cases. For Jake's table, 100 million orders of 2.5 KB with 10 million writes a month (2 million new orders, 3 million status changes and 5 million other updates), the designs compare like this:

DesignWrite unitsIndex storageMonthly total
No indexes30 million0$75.00
by-email ALL + by-status KEYS_ONLY68 million278 GB$168.25
by-email INCLUDE 200 bytes + by-status KEYS_ONLY43 million48 GB$95.12
Same, with a sparse status index (open orders only)40 million34.7 GB$89.92

The two indexes as first built more than doubled the bill, from $75 to $168.25, and almost all of the increase came from the ALL projection: 30 million extra write units and 264 GB of copies. Projecting only the 200 bytes the email lookup actually returns (name, status and date) brought it to $95.12. The INCLUDE version also writes the email index only when one of those attributes changes, which is why the write units fall so far. In the sparse design, only open orders carry the status key: 2 million new orders enter the index, 1 million status changes move between open states (two writes each), and 1 million orders leave it when they close.

KEYS_ONLY, INCLUDE or ALL: choosing a projection

ProjectionWhat the index storesWrite costUse it when
KEYS_ONLYTable key and index keyLowest: written only when an item enters, leaves or changes its index keyYou need the list of matching keys, or fetch the few items you show
INCLUDEKeys plus the attributes you nameAlso written when a named attribute changesThe query returns a known, small set of attributes
ALLThe whole itemHighest: written on every change, at the full item sizeSmall items, or a query that truly needs every attribute and runs constantly

A practical rule: start with INCLUDE and only the attributes your screen or API response shows. You cannot change an index's projection after it is created; to change it, create a new index with the new projection, switch your queries, then delete the old one.

Sparse indexes: pay only for the items you query

An item is in a GSI only if it has the index key attribute. That gives you a free filter: if you only ever query "open orders", give open orders an attribute such as open_since and remove it when the order closes. The index then holds only open orders, perhaps 5% of the table, and every closed order costs nothing in the index. Line 2 of our test shows the effect: a write without the index key cost 1 unit on the table and nothing on either index.

Sparse indexes are also faster to query, because there is less in them. The trade-off is that removing the attribute is itself an index write (one delete), so a sparse index pays off when items leave it once and stay out.

Indexes on attributes that change all the time

The most expensive index key is one that changes on most writes: status in a workflow, updatedAt, a counter, a score. Each change costs 2 index writes, and each change also moves the item from one index partition to another. An index whose partition key has only a few values, such as status, is the worst case: every open order shares the key open, so that one index partition takes all of those writes and can throttle even when the table has plenty of room. Our hot partition guide covers the throttling side.

Better options: index the attribute that rarely changes and filter on the changing one; keep "current status" items in a separate, small sparse index; or write events to a DynamoDB stream and maintain the view you need somewhere cheaper to update.

Provisioned mode: an under-sized GSI throttles your table

In provisioned mode, each GSI has its own read and write capacity, separate from the table's. This is where indexes cause outages, not just bills: if a GSI does not have enough write capacity for the writes the table sends it, DynamoDB throttles writes to the table. Your application sees throttling on PutItem and UpdateItem against the table, even though the table's own capacity looks fine.

To keep an index from throttling the table:

  1. Give every GSI at least as much write capacity as the table, and more if items change index keys often (2 writes each) or index entries are over 1 KB.
  2. Turn on auto scaling for each index, not just the table.
  3. Watch WriteThrottleEvents on the index (dimension GlobalSecondaryIndexName) as well as on the table.

In our example, 10 million writes a month is only about 3.8 writes per second on average, so provisioned capacity would be cheap; the danger is the busy hour, when the table and every index must keep up at the same time. Our provisioned throughput guide shows how to size capacity from peak traffic.

What it costs to add a GSI to an existing table

When you add an index to a table that already has data, DynamoDB fills it in two phases: it allocates resources, then backfills by reading every item and writing the ones that have the index key. You are not charged for the reads DynamoDB makes from the table to fill the index. Budget for the index's writes and its storage from day one: on a large table, the new index starts with millions of entries.

While it builds:

  • The index is CREATING and cannot be queried until it is ACTIVE.
  • You cannot add or delete other indexes on the table, and you can delete only one index per UpdateTable call.
  • On a provisioned table, watch the table's WriteThrottleEvents during the build, and be ready to cancel the index if your application is throttled. Raising the index's write capacity does not make the build faster.
  • OnlineIndexPercentageProgress in CloudWatch shows how far it has got.

Even on an empty table, creating an index is not instant, because the resource allocation phase still runs; check IndexStatus with describe-table rather than waiting a fixed time. You can have up to 20 GSIs per table by default.

GSI vs LSI: which one costs less?

ItemGlobal secondary indexLocal secondary index
KeyAny partition key and sort keySame partition key as the table, different sort key
CapacityIts own (provisioned) or its own request units (on-demand)Shares the table's capacity
ReadsEventually consistent onlyStrongly consistent available; can fetch unprojected attributes from the table (at extra read cost)
Size limitNone per key10 GB per partition key value, table and indexes together
When you can add itAny timeOnly when the table is created
How many20 per table (default quota)5 per table

Both store a copy of the projected data and both cost index writes, so neither is free. An LSI makes sense when you query within one partition key, need strongly consistent reads, and are certain no partition key will grow past 10 GB. For everything else, a GSI is the safer default, because you can add, change or drop it later.

Standard-IA: when the index storage is the problem

If most of your DynamoDB bill is storage (large tables and fat indexes that are rarely read), the Standard-Infrequent Access table class can help. It charges $0.10 per GB-month instead of $0.25, but $0.78 per million writes instead of $0.625. Indexes always follow their table's class, so switching the table switches the indexes too. In our example, the 278 GB of index copies would cost about $27.80 a month on Standard-IA instead of $69.50, while the 68 million write units would rise from $42.50 to $53.04. Check the balance for your own table before you switch.

How to see what each index costs you

Your bill lists the table's request units and storage, and index usage is added into those lines rather than shown per index. To see each index's share:

  • In code: pass ReturnConsumedCapacity="INDEXES" on writes and queries, as in our test, and log the per-index numbers for a sample of requests.
  • In CloudWatch: ConsumedWriteCapacityUnits and ConsumedReadCapacityUnits with the dimensions TableName and GlobalSecondaryIndexName show each index's usage over time. Compare the index's write units with the table's: an index using more than the table is a sign of a fat projection or a frequently changing key.
  • For storage: describe-table reports IndexSizeBytes and ItemCount for each GSI (refreshed about every six hours).
aws dynamodb describe-table --table-name orders \
    --query "Table.GlobalSecondaryIndexes[].{Index:IndexName,Bytes:IndexSizeBytes,Items:ItemCount,Projection:Projection.ProjectionType}"

If you use CloudWatch for the first time, our plain-English CloudWatch guide shows how to graph a metric with two dimensions.

How to cut your GSI cost, step by step

  1. List every index and the query it serves. Delete indexes no code uses; they cost writes and storage every month for nothing.
  2. Measure each index with CloudWatch's per-index write units and IndexSizeBytes.
  3. Replace ALL projections with INCLUDE and only the attributes the query returns (a new index, then delete the old one).
  4. Make indexes sparse where queries only need a subset of items.
  5. Stop indexing attributes that change on most writes, or move them to a small sparse index.
  6. On provisioned tables, give each index enough write capacity and auto scaling, so a cheap index does not cause an expensive outage.
  7. Consider Standard-IA if storage dominates and the table is rarely read.

Ethan did steps 3 and 4 on Jake's table. The email index now projects the customer's name, the order status and the date, and the status index only holds orders that are still open.

DynamoDB GSI cost: frequently asked questions

Does a GSI cost extra in DynamoDB?

Yes. Every write that changes an item's index entry costs write units on the index, every query on it costs read units, and you pay storage for every item in it, on top of the table's own costs.

How many write units does a GSI write cost?

One per 1 KB of the index entry (table key, index key and projected attributes), rounded up, for each write that adds, updates or removes the entry. Changing the index key costs two.

Why does updating a GSI key cost two writes?

DynamoDB deletes the item's entry under the old key and puts a new entry under the new key, and each is a separate index write.

Do GSIs double the cost of DynamoDB?

They can more than double it. In our example, two indexes took the bill from $75.00 to $168.25 a month, mostly because one index projected all attributes; a narrower projection brought it to $95.12.

How is GSI storage calculated?

Each index item is the table's primary key size plus the index key size plus projected attributes plus 100 bytes of overhead, charged at $0.25 per GB-month on the Standard table class.

Does a sparse index save money?

Yes. Items without the index key are not written to or stored in the index, so a write to such an item costs only the table's write units.

Can I query a GSI with strongly consistent reads?

No. DynamoDB rejects it with "Consistent reads are not supported on global secondary indexes". GSI reads are eventually consistent only.

Why are DynamoDB global secondary indexes still eventually consistent?

DynamoDB updates indexes asynchronously after the table write succeeds, so an index can lag the table briefly. If you need read-after-write consistency, read the table by its primary key.

What is the cheapest GSI projection?

KEYS_ONLY, because its entries are small and it is written only when an item enters or leaves the index or changes its index key. INCLUDE with a few attributes is a good middle ground.

Does an under-provisioned GSI throttle the base table?

Yes, in provisioned mode. If an index lacks write capacity for the writes the table sends it, DynamoDB throttles writes to the table itself. Give each index at least the table's write capacity.

Am I charged for backfilling a new GSI?

You are not charged for the reads DynamoDB makes from the table to fill the index. The index's writes and its storage are part of what it costs to have the index.

How long does it take to create a GSI on an empty table?

Not instant: DynamoDB still allocates resources before the index becomes ACTIVE. Check IndexStatus with describe-table instead of waiting a fixed time.

What's the best way to query a frequently updating field?

Avoid making it an index key, because every change costs two index writes and moves the item between partitions. Index a stable attribute and filter on the changing one, or keep a small sparse index for current items.

How can I reduce costs associated with DynamoDB?

Delete unused indexes, project fewer attributes, use sparse indexes, keep items small, choose the right capacity mode for your traffic, and consider Standard-IA for large, rarely read tables.

How do I query a GSI with more than 1 MB of data?

A single query returns at most 1 MB. Keep querying with ExclusiveStartKey set to the LastEvaluatedKey from the previous page until no LastEvaluatedKey comes back.

GSI or LSI: which is cheaper?

Both cost index writes and storage. An LSI shares the table's capacity and allows strongly consistent reads, but limits each partition key to 10 GB and must be created with the table; a GSI is more flexible.

How many GSIs can a DynamoDB table have?

20 by default, and you can request more. Each one adds write, read and storage costs, so most tables need far fewer.

Do DynamoDB GSI keys need to be unique?

No. Table keys must be unique, but many items can share the same index key value, which is exactly what lets you list every order with the status open.

What happens if the GSI key attribute is missing from an item?

The item is simply not written to the index, at no index cost. That is how sparse indexes work. An item whose index key has the wrong data type is rejected by the table write instead.

How long does GSI eventual consistency take?

Under normal conditions, table changes reach the index within a fraction of a second, but longer delays are possible, so code that queries an index right after a write must handle not seeing it yet.

How do I see how much each GSI costs?

Use ReturnConsumedCapacity INDEXES on requests, CloudWatch's ConsumedWriteCapacityUnits with the GlobalSecondaryIndexName dimension, and IndexSizeBytes from describe-table.

For readers preparing for an AWS certification

We have been covering AWS certifications alongside our fixes, so here is a short extra for readers studying for one. If you only came for the fix, you already have it; feel free to skip this part.

Exams that test this topic: Developer – Associate (DVA-C03 from December 1, 2026) • Solutions Architect – Associate (SAA-C03) • CloudOps Engineer – Associate (SOA-C03).

Here are the exam-style questions this topic tends to raise, with the reasoning you need to answer them:

1. Writes to a provisioned DynamoDB table are being throttled, but the table's own write capacity is far from fully used. What is the most likely cause?

A global secondary index on the table has too little write capacity; DynamoDB throttles table writes when an index cannot absorb them. Raise the index's write capacity or enable auto scaling on it.

2. A GSI uses the frequently changing attribute status as its partition key, and write costs are higher than expected. Why?

Each change to an index key costs two index writes (delete the old entry, put the new one), and a low-cardinality key also concentrates writes on few index partitions.

3. An application must read an item through a query immediately after writing it and always see the new value. A GSI query sometimes returns old data. What should the developer do?

GSIs support only eventually consistent reads. Read the base table by its primary key with a strongly consistent read.

4. A GSI that serves an order-summary screen projects ALL attributes, and its storage and write costs are high. How can cost be reduced without losing the screen's data?

Create a replacement index with an INCLUDE projection of only the attributes the screen needs (or KEYS_ONLY), switch queries to it, then delete the old index.

5. A table holds millions of orders, but only open orders are ever queried by status. How can the index cost be minimized?

Use a sparse index: set the index key attribute only on open orders and remove it when an order closes, so closed orders are not written to or stored in the index.

6. Which DynamoDB index type supports strongly consistent reads, and what limits come with it?

A local secondary index: same partition key as the table, created only with the table, up to 5 per table, and a 10 GB limit per partition key value.

Exam codes and dates change often. Our AWS certification changes page keeps track of what is current.

πŸ“š ALSO READ

More DynamoDB guides for the same project:

📌 Bookmark this page. Run the calculator again whenever you add an index, because each one changes the cost of every write.

A month later, the DynamoDB line on Jake's bill was close to where it had been before the indexes, and both lookups still worked. "So the indexes weren't the problem," Jake said. "Copying every order into one of them was," Ethan said. If your numbers differ from what this page shows, please tell us. Guides like this stay right because readers write in.

📌 If you keep one line from this page

Every GSI is a second table you pay to write: project only what the query returns, keep items out of the index unless you query them, and never key an index on an attribute that changes on every write.

$0.625 per million writes · $0.25 per GB-month · 100 bytes per index item · key change = 2 writes · US East, October 24, 2026.

Revision note. Written October 24, 2026, from a DynamoDB Local 3.3.1 test and that day's US East prices. "Penny wise, pound foolish," the old saying warns, and an index that copies everything is the pound.

Related