When a DynamoDB vector index is actually ready
AWS shipped vector search for DynamoDB on 5th August, straight to GA. I spent a couple of days pinning it down against real DynamoDB so I could add it to my conformance suite, which meant reading the docs properly and then checking whether they were true.
Mostly they are. But there's one thing they get wrong three different ways, and the three stack up: knowing when a vector index is ready to search.
The tutorial contradicts everything else
Add a vector index to a table that already has data in it and there's a gap before you can use it, while DynamoDB backfills the index from the items you already have. What happens if you search during that gap depends on which page you read.
Three pages say you get an error. The data synchronisation page puts it plainly:
New writes to the base table are replicated to the index during this phase, but
SearchVectorsreturns an error until backfilling finishes.
Creating and searching has a callout headed "You can't search while the index is backfilling", and troubleshooting carries a row for it. The tutorial says something different:
Searching an index that is not yet
ACTIVEfails, and searching during backfill can return incomplete results.
Those aren't the same problem at all. An error you catch and retry. Incomplete results you can't spot, because a short answer from an approximate search looks exactly like a short answer from a thin index. If the tutorial were right, every search during a backfill would be quietly wrong with nothing to tell you.
It isn't right. You get an error, and there are two of them depending on how far along the index is:
ValidationException: The table does not have the specified index: vix
ValidationException: Cannot search backfilling vector index: vix
So trust the other three. Worth saying out loud, because the tutorial is the page you'd be following if this is your first vector index.
The state machine doesn't exist
This is the one that would actually bite.
Those three pages all describe the index moving through the same three states: CREATING while DynamoDB sets up the infrastructure, then a middle state, then ACTIVE with Backfilling: false once it's done. The data synchronisation page names the middle one:
It then moves to
IndexStatusACTIVEwithBackfillingset totruewhile DynamoDB populates the index from existing base table data.
Every piece of waiting advice in the documentation hangs off that state.
I polled DescribeTable every five seconds through the creation of an index added to a table that already had items in it. This is what I got:
CREATING Backfilling: false
CREATING Backfilling: true
ACTIVE Backfilling: (gone)
There's no middle state. Backfilling is a CREATING-time field, and when the index flips to ACTIVE the field doesn't settle to false, it vanishes from the response.
That breaks the advice, because the advice tells you to wait for a value that never arrives. Data synchronisation again, in the sentence straight after the one I quoted earlier:
Use
DescribeTableand wait untilIndexStatusisACTIVEandBackfillingisfalsebefore you search.
Write that literally and your poll never finishes. By the time the status is ACTIVE, Backfilling is undefined, not false. One page out of the four allows for that, and it does it in a parenthesis: "Backfilling set to false (or absent)".
Create the index as part of CreateTable instead of adding it later and the field never turns up at all. That one I do assert, on every poll, so if AWS starts reporting it my tests break:
expect(ix?.Backfilling).toBeUndefined()
Let me be straight about the evidence, though, because the two halves aren't equally solid. That assertion is real and runs on every scheduled run. The three-line transcript above isn't pinned: it's something I recorded on 11th August, and the test only checks the field showed up at some point, without checking that true appears solely alongside CREATING. It's in the capture file with the documented version quoted next to it if you want to check the mismatch yourself. A gap in my tests rather than in the finding, and it's on the list.
ACTIVE arrives too early
Even once the status says ACTIVE, the index can still not be ready.
DescribeTable goes ACTIVE a beat before the search plane agrees. Search on that first ACTIVE and you get failures that look like flakiness and aren't. It's a small window, which is worse than a big one: a window small enough to survive your local testing still opens wherever things run fastest.
That bites hardest when you create the index with the table, because there's no Backfilling field on that path and the tutorial tells you to go on status alone:
Use
IndexStatusalone as your signal in that case.
Status alone is the signal that arrives early.
How to wait for it properly
Prove the index is ready with a search, not with a description. My suite polls until a search comes back with everything it put in:
while (true) {
try {
const res = await ddb.send(new SearchVectorsCommand({
TableName, IndexName, SearchVector, TopK: 5,
}))
if ((res.SearchResults ?? []).length === expectedCount) break
} catch (err) {
// Not ready yet. Anything else is a real failure.
if (err.name !== 'ValidationException') throw err
}
await new Promise(r => setTimeout(r, 5000))
}
The count check earns its place as much as the try/catch does. A search can start succeeding while the index still has a partial view of your data, so a bare "did it throw" test lets you through early with fewer results than you wrote.
Budget some time for it too. An index added to a table holding 25 items took about 17 minutes to become searchable. It's running on the same online-index machinery as a GSI, and the size of the table isn't what drives that number. It inherits the GSI concurrency limit as well, error message and all:
LimitExceededException: Subscriber limit exceeded: Only 1 online index
can be created or deleted simultaneously per table
Other things worth knowing
The index keeps your embeddings at f32 and the table doesn't. The docs do say this, so it's not a discovery, but it's worth seeing how big the gap is. 16777217 is the first integer a 32-bit float can't hold. Write it into an embedding and the base table gives it back exactly. Ask for the same attribute through a search projection and you get 16777216. The index also prints its f32 copy as the shortest decimal that names the float, so 0.1 comes back as 0.1 rather than 0.100000001490116119384765625, which makes the whole thing harder to notice than it sounds.
Vector capacity has a shape nothing else in DynamoDB uses. A search reports a bare object with no capacity units and no table name:
{ VectorSearchRequestBytes: 1024 }
Writes report a VectorIndexes map, but only under ReturnConsumedCapacity: 'INDEXES'. Ask for TOTAL and you get nothing vector-related at all, not even rolled into the total, so the two modes aren't detail levels of the same number here.
That 1024 is interesting on its own. A three-dimension vector is twelve bytes of f32, so it looks like a 1KB floor rather than anything to do with the size of your vector. One run isn't enough to assert a formula, so my tests check it's positive and stop there.
No emulator implements any of this yet. I score eight DynamoDB emulators against real AWS on every run and all eight skip the whole family. DynamoDB Local is the one to watch: it takes VectorIndexes on CreateTable quite happily and then silently drops them, so a DescribeTable afterwards shows no indexes at all. If you're testing locally against it, the vector index you think you created isn't there.
You need a recent SDK. @aws-sdk/client-dynamodb 3.1103.0 or later.
The evidence
Both sides of the backfill contradiction are in captures/2026-08-12-vector-backfill-docs.json, dated, with their URLs and anchors, along with the documented state machine, the transcript that doesn't match it, and which of the two paths is pinned by an assertion. Check any of it rather than taking my word for it.
The tests live in the same repo under tests/tier2/vectorSearch and run against real DynamoDB on a schedule, so if AWS changes something here I'll find out. If you've hit anything else in it, tell me and I'll add a test.