---
title: "DynamoDB changed how it bills a vector read - Martin Hicks"
description: "A DynamoDB read of an item with a vector attribute cost 2 read units on 9th October 2026 and 1 on 22nd October. What changed, what didn't, and what I can't explain."
canonical: https://martinhicks.dev/articles/dynamodb-vector-read-billing
last-updated: 2026-10-23
---

![The Parity Suite logo, a teal rounded square with three descending bars, on a near-black background above the paritysuite.org wordmark](https://martinhicks.dev/images/articles/parity-suite-card.png)23rd October 2026

# DynamoDB changed how it bills a vector read

On 9th October I pinned a test in [Parity Suite](https://paritysuite.org) that said a read of a particular DynamoDB item costs 2 read units. On 22nd October, while getting the 3.7.0 release ready, the same read against real DynamoDB cost 1. Same item, same request. I can't find anything from AWS saying it changed.

The item has a vector in it, and vectors are the one place I know of where DynamoDB keeps two sizes for an item.

## Two sizes for one item

A vector in DynamoDB is a list of numbers. Stored, each number is sized the way DynamoDB sizes any number: roughly one byte per two significant digits, plus one. A 38-digit integer is 20 bytes. The list adds 3 bytes, and 1 more per element.

Billed, it's different. Once a vector index reads the attribute, a write charges it at its name plus 4 bytes per dimension, whatever numbers are in it. That's the size of a 32-bit float, which is presumably what the index holds. `DescribeTable`'s `TableSizeBytes` uses the same figure, and AWS's [pricing page](https://aws.amazon.com/dynamodb/pricing/on-demand/) treats a 768-dimension vector as 3,072 bytes when it works out the base table's cost.

So an item can be 4KB as far as billing is concerned and over 4KB on disk. The question is which of the two a read goes by.

## The read

The test builds exactly that item: six dimensions of 38-digit integers, padded so it's 4,096 bytes billed and 4,201 bytes stored. Then it does a strongly consistent `GetItem` and asks for the consumed capacity.

A read unit covers 4KB, so the answer tells you which size the read used. 2 units means the stored size, 1 means the billed size.

On 9th October, in eu-west-2, it was 2. That came out of an evening of bisecting the boundaries with a padding string, so every figure was exact to the byte. Writes went by the billed size, in eu-west-2 and us-east-1. Every read I tried went by the stored size: this one, plus `GetItem` and `Query` either side of 8KB, strongly and eventually consistent. So did the 400KB item limit. I wrote the tests to say so, and the read test passed against eu-west-2 that night.

On 22nd October the same test, unchanged, failed in eu-west-2 with 1. I ran it in every region, and it was 1 in all 32 that offer vector search. The 400KB limit hasn't moved. An item whose numbers take it over 400KB is still refused, even when its billed size is well under.

My first thought was that I'd got it wrong on the 9th, because I'd changed Dynoxide to match in a hurry that same evening. But the raw responses from that capture are saved, and they say 2. It's the same item, the same request and the same region, and the answer has changed.

## What I think happened

I can't see inside DynamoDB, so this part is a guess, and I could be wrong about it. When vector search launched, writes and storage were metered on the billed size, but reads still went through the ordinary item-size calculation every other item uses. That made a read of a vector item cost more than the pricing page implies, and somebody brought it into line. The 400KB limit staying put fits that too. It's a limit on what's actually stored rather than a billing figure, so there's no reason for it to follow.

What I can't tell you is when. The suite only runs against real AWS on a schedule or when a change lands on main, never on a pull request, because every run shares one AWS account and costs money. This test sat on a branch from 9th October to 22nd October, so nothing asked DynamoDB the question in between. Some time in those two weeks is as close as I can get.

I also only re-measured a consistent `GetItem`. Eventually consistent reads, `BatchGetItem`, `TransactGetItems`, `Query`, `Scan` and PartiQL reads might have moved with it, or might not. They're next to capture.

## Where the stored size still matters

If you're paying for reads, the change works in your favour, though for most vector items it won't make a visible difference. Where the stored size still counts is the 400KB limit, and the gap between the two sizes depends on how many digits your numbers carry.

A float32 written out to 9 significant digits is about 6 bytes stored, against 4 billed. But if your embedding comes back as a `Float32Array` in JavaScript and you write the values as plain numbers, each one is widened to a double first, and `0.1` becomes `0.10000000149011612`. That's 17 significant digits, about 10 bytes stored, plus 1 for the list element. A 768-dimension vector written that way is around 8,450 bytes on disk and 3,072 on the bill.

With one vector per item that's nowhere near 400KB. With several vectors per item, or a large document sitting next to one, the stored size is the one you'll hit.

## Dynoxide is now wrong about this

[Dynoxide](https://dynoxide.dev) copied the 9th October behaviour, because that's what DynamoDB did when I measured it. It still charges 2 units for that read, so as of 3.7.0 it fails a test it used to pass. Floci and ExtendDB's SQLite build fail it the same way. The fix is tracked in [#264](https://github.com/nubo-db/dynoxide/issues/264), and it waits on measuring the other read paths first, so I don't move one and leave the rest behind.

## The rest of 3.7.0

The vector test is one of 412 new ones. The suite went from 1,266 tests to 1,678, mostly on rejection paths and the order in which errors are reported, where an emulator giving the wrong answer used to score a pass because nothing checked.

That took a letter off most of the board. ExtendDB, LocalStack and Ministack go from B to C, DynamoDB Local and Dynalite from C to D, and Dynoxide's browser build from A to B. Dynoxide's native build stays an A, though its divergence goes from 0.8% to 2.2%. Floci went the other way, from C to B, with its Tier 2 divergence down from 27.5% to 6.2% on a newer build.

The board is at [paritysuite.org](https://paritysuite.org), and the [changelog](https://github.com/paritysuite/dynamodb-conformance/blob/main/CHANGELOG.md) lists everything 3.7.0 added.

## Further reading

-    [When a DynamoDB vector index is actually ready
    
    AWS said to wait for IndexStatus ACTIVE and Backfilling false. I measured it, and that state never happens. AWS have since rewritten the docs.](https://martinhicks.dev/articles/dynamodb-vector-search-docs-get-wrong)
-    [Running DynamoDB vector search locally
    
    Dynoxide runs DynamoDB vector indexes and SearchVectors on your own machine. What I measured about the index lifecycle before building it, where it will never match AWS, and how a validation-error rollout I caught in June finally ended.](https://martinhicks.dev/articles/running-dynamodb-vector-search-locally)
-    [I watched DynamoDB change under my conformance suite
    
    My DynamoDB conformance suite went red and the test that failed was real AWS. It had reworded its validation errors, and the change hasn't reached every region yet.](https://martinhicks.dev/articles/dynamodb-changed-its-validation-errors)

---

Source: https://martinhicks.dev/articles/dynamodb-vector-read-billing
