Skip to content

The cost model ​

Keyway estimates the capacity units each write consumes, computed from the item's size and the indexes it touches, so you can see why a write costs what it does. It follows DynamoDB's documented formulas, but it is a model-based estimate, not your actual bill. The math lives in src/engine/itemsize.ts (sizes and units) and src/engine/cost.ts (per-op, per-index effects).

Item size ​

An item's capacity-relevant size is the sum, over its attributes, of the UTF-8 byte length of the name plus the value:

text
itemSize(item) = Σ  utf8Len(name) + utf8Len(value)

Notes that keep this honest:

  • Bytes are counted as UTF-8, not JavaScript .length (which counts code units), so multi-byte characters cost their real size.
  • Every value is sized as a DynamoDB String (S): its UTF-8 byte length. The model is flat and all-strings today; Number/Binary/Set/Map sizing lands with typed attributes.
  • The 100-byte per-item overhead from the AWS docs is storage overhead, not capacity, so it's excluded here. The 400 KB hard item limit is tracked as MAX_ITEM_BYTES.

Write capacity (WCU) ​

text
WCU = ceil(size / 1KB) × (1 standard | 2 transactional)      # min 1
  • 1 WCU per 1 KB, rounded up, minimum 1.
  • A transactional write bills double: its base writes cost 2×.

Read capacity (RCU) ​

text
RCU = ceil(cumulativeBytes / 4KB) × (0.5 eventual | 1 strong | 2 transactional)   # units min 1
  • 1 RCU per 4 KB, rounded up, minimum 1 unit.
  • Eventually consistent reads are half; strong is full; transactional is double.
  • For a Query/Scan, pass the cumulative size of every item read: DynamoDB rounds the total once, not per item. This is why a filter that drops most rows still leaves the cost high: cost is computed from what was read, not what was returned (see Filters).

Index effects: what a write does to each index ​

Every write is priced per index by its transition, not a snapshot. The vocabulary (IndexEffect):

EffectMeaningWrites
nonethe write doesn't touch this index (no key before or after)0
insertthe item enters the index for the first time1×
deletethe item leaves the index (a key attr removed, or the row deleted)1×
updatethe item stays put, but a projected attribute changed1×
reindexthe index key changed → delete the old projection and put a new2×

A GSI write is sized by the projected item, so a KEYS_ONLY or INCLUDE index that carries less data costs less. An update only fires when an attribute the index actually projects changed: a KEYS_ONLY GSI ignores changes to attributes it doesn't carry.

Why a key change is a 2-write reindex ​

DynamoDB can't move a projected row to a new key in place. When an index key changes, it deletes the old projection and puts a new one: two writes on that index. In Keyway this is the reindex effect, and it's the classic cost that bites people: flipping GSI1PK=STATUS#pending to STATUS#shipped is a 1-write update on the base table but a 2-write reindex on GSI1.

Transactional writes bill base at 2× ​

A repeated label with a new base key becomes an atomic TransactWriteItems (a delete-old + put-new, see the DSL). For a transact:

  • Base writes are billed at 2× (the transactional rate). So an atomic key rename is 2 × (delete + put) = 4 base WCU: the price of doing it safely instead of as two racy writes.
  • GSI maintenance is billed at the standard rate even inside a transaction, because index propagation is asynchronous and outside the transaction's guarantee.

Multi-key GSI query rules ​

A native multi-key GSI has up to 4 partition and 4 sort attributes as separate columns. When you query one:

  • Every partition attribute must be supplied with equality.
  • Only the last sort attribute may take a range; every earlier sort attribute must be =.
  • No skipping sort attributes: conditions apply as a left-to-right prefix.
text
@gsi ByRegion pk=tenant,region sk=status,date

@ap Open orders for a tenant+region -> ByRegion tenant=acme region=us status=open
#   tenant= and region= are equality (both partition attrs); status= is the
#   first sort attr as equality; add `date between …` as the last, rangeable one.

Break a rule and the query returns a naming error (e.g. "only the last sort key (date) can use a range; status must be =") rather than misbehaving: the same error that flags a pattern invalid under coverage.

A personal project. Opinions are my own, not those of AWS or Amazon. Amazon DynamoDB, AWS, and NoSQL Workbench are trademarks of Amazon.com, Inc. or its affiliates.