Documentation menu

Compatibility and limitations

Conformance answers "does Dynoxide behave like DynamoDB on the things it implements". This page answers the other half: what it implements, what it doesn't, and where it knowingly differs.

What's implemented

Every DynamoDB operation with a meaningful local equivalent: item CRUD, Query and Scan, batch reads and writes, transactions, table management, PartiQL, vector search, streams, TTL, and resource tags. GSIs and LSIs are both supported, including UpdateTable adding or removing a GSI with existing rows backfilled into it.

All ten attribute types work (S, N, B, BOOL, NULL, SS, NS, BS, L, M), and the full expression syntax is supported: condition, filter, key-condition, projection and update expressions, with attribute_exists, attribute_not_exists, attribute_type, begins_with, contains and size.

That's every applicable operation: 28 of 28, with SearchVectors joining at 1.0.0. The compatibility summary has the operation-by-operation table, and the live capability matrix compares every emulator feature by feature.

Multi-attribute GSIs

From 1.3.0, a GSI can have up to four partition key attributes and four sort key attributes. Declare partition keys first, without repeating a name. Base tables and LSIs keep their usual key shape; an LSI has one sort key attribute.

A Query must give an equality for every GSI partition key attribute. Sort key conditions must cover a leading run of attributes, with a range condition allowed only on the last one named. Items sort by each sort key attribute in turn, an item missing any index key attribute is excluded, and LastEvaluatedKey carries every key attribute. UpdateTable can add these indexes to an existing table.

PartiQL SELECT on a multi-attribute GSI is refused with PartiQL statements cannot be executed on tables with multi-attribute HASH keys or multi-attribute RANGE keys. The base table and its other indexes remain usable. Persistent indexes written by an older build are rebuilt on opening the database; read the upgrade guidance before updating.

What's not implemented, and why

Thirty-nine DynamoDB operations are absent on purpose. They're cloud-infrastructure features with nothing to emulate locally: backup and point-in-time restore, global tables, Kinesis streaming destinations, table import and export, reserved capacity and account limits, contributor insights, resource policies, and table replicas.

These are "not applicable" rather than "not implemented". There's no partial version of a global table on your laptop. Call one and you get an UnknownOperationException.

AWS added vector search to DynamoDB in August 2026: vector indexes declared on CreateTable and adjusted through UpdateTable, a SearchVectors operation, and PartiQL syntax for querying them. Dynoxide implements it as of 1.0.0, on the native build and the browser build together.

CreateTable and UpdateTable take vector index definitions, DescribeTable reports them alongside the GSIs, the item write paths maintain them, and SearchVectors returns the nearest entries to a query vector over COSINE, EUCLIDEAN or DOT_PRODUCT. A search can be scoped by a SearchSchema hash attribute and filtered on INLINE_FILTER attributes. Index maintenance is atomic with the base write, the same as it is for a GSI. The MCP server gains a search_vectors tool with it.

An index added to a live table through UpdateTable walks a creation lifecycle rather than arriving ready, and it walks the same four phases DynamoDB walks, including the trap: the table reports ACTIVE, and then the index reports ACTIVE, some way before a search against it will answer. Poll by retrying the search and treating the refusal as "not yet" rather than waiting on a status. An index created as part of CreateTable is searchable at once, on both.

Parity Suite covers vector search across Tier 2 and Tier 3, and those tests were the largest block Dynoxide skipped before 1.0.0. A skip is scope rather than a wrong answer, so it left the divergence figure alone and lowered coverage instead. Conformance explains why the two are reported apart.

Four things about vector search that will never match AWS

These aren't bugs and they aren't going to be fixed. They follow from how the two systems are built, and each one can make a test pass here and fail against AWS.

Dynoxide finds the genuinely nearest vectors; AWS finds roughly the nearest ones. Dynoxide compares your query against every vector in the index. AWS builds an approximate index that skips most of those comparisons to stay fast on very large tables, and skipping comparisons means it can miss a close match that was really there. Don't pin the exact result list in a test that also runs against AWS. The same trade-off runs the other way on speed: because Dynoxide checks every vector, a local index holding millions of them is slow in a way AWS wouldn't be.

Ties are ordered stably here and arbitrarily there. Dynoxide breaks a tie by primary key, so the same query over the same data gives the same order every time. AWS makes no such promise, and three identical calls against real DynamoDB returned three different orderings. Sort ties yourself if you need an order you can assert on.

The creation lifecycle has AWS's shape and nothing like its length. The phases, the statuses and the refusals all match. The clock doesn't: Dynoxide runs the whole thing in about half a minute, where AWS takes minutes, and how many depends on how much data it has to index. Never sleep(30) and assume the index is ready.

VectorSearchRequestBytes is Dynoxide's own number. AWS bills a search on whatever data it happened to read, and it doesn't reproduce its own figure between identical calls. Dynoxide reports a stable figure sized on the entries it scanned, in the same unit AWS uses. Compare it between Dynoxide searches, where it means something, not against a figure from AWS. The write-side figure, VectorWriteRequestBytes, is captured from real DynamoDB and does match.

The compatibility summary has the lifecycle table and the rest of the detail.

Behavioural differences

These differences are visible from test code, alongside the vector-search differences above.

ConsistentRead changes capacity reporting, but every read is strongly consistent. A bug that only shows up under eventual consistency won't surface locally.

Streams expose a single shard. DescribeStream returns one shard, and ExclusiveStartShardId and Limit are accepted but ignored. Code that walks a multi-shard topology or handles shard splits has no local equivalent to exercise. See Streams.

Transaction-contention errors aren't emulated. TransactionConflictException and TransactionInProgressException never fire, because there's no concurrent contention in a single process. If your retry logic keys off them, it won't be exercised locally.

Number arithmetic uses bigdecimal. DynamoDB's implementation is proprietary, so agreement on captured cases does not guarantee agreement at every numeric edge.

PartiQL has its own complexity limits. A WHERE clause is expanded into alternatives internally. Dynoxide allows at most 256 alternatives, 4,096 condition slots in one alternative and 16,384 across them. DynamoDB evaluates a tree instead and can run clauses these limits refuse. See PartiQL limits.

Legacy pre-2015 parameters

The pre-expression API is implemented:

Parameter Support
AttributeUpdates (UpdateItem) PUT, ADD and DELETE actions
Expected (PutItem, UpdateItem, DeleteItem) Value and Exists, or ComparisonOperator with AttributeValueList
ScanFilter / QueryFilter EQ, NE, LE, LT, GE, GT, NULL, NOT_NULL, CONTAINS, NOT_CONTAINS, BEGINS_WITH, IN, BETWEEN
KeyConditions (Query) EQ on partition keys; EQ, LE, LT, GE, GT, BEGINS_WITH or BETWEEN on sort keys
AttributesToGet (GetItem, BatchGetItem, Query, Scan) Returns named top-level attributes
ConditionalOperator AND or OR joining two or more Expected, QueryFilter or ScanFilter conditions

Mixing a legacy parameter with its expression counterpart is refused. Prefer expressions for new code: they also support nested paths and functions.

Product 1.3.0 tightens legacy validation. Malformed conditions and invalid values are refused rather than dropped, unknown comparison operators are rejected, and operator/action names must use their defined case (EQ, PUT, and so on). Legacy attribute names containing dots, hyphens or non-ASCII characters work as names.

Capacity and statistics

Dynoxide reports ConsumedCapacity when requested, including index breakdowns with ReturnConsumedCapacity: INDEXES. It doesn't enforce provisioned throughput or reproduce AWS load-based throttling. ConsistentRead affects the reported read charge, even though every local read is strongly consistent. The MCP tools can report capacity too.

From 1.3.0, DescribeTable and UpdateTable report live ItemCount and TableSizeBytes, and live ItemCount and IndexSizeBytes for GSIs, LSIs and vector indexes. AWS refreshes these statistics about every six hours, so don't expect its timing locally. ItemCollectionMetrics.SizeEstimateRangeGB returns whole-GB bounds: a small collection reports [0.0, 1.0].

On a table with a vector index, write charges and TableSizeBytes count a vector at four bytes per dimension. A vector index's size also includes the attribute name and the rest of its projection. Read charges and the 400 KB item limit still size the vector as its numbers; so do GSI/LSI write charges and sizes when they project it. Existing vector table sizes are recomputed during the database upgrade.

Changes to test assertions in 1.4.0

Every clause of an update reads the item as it was before the update, in UpdateItem and PartiQL UPDATE. REMOVE l[0], l[1] removes both elements, and SET n = 7, s = n + 1 gives s the old n plus one. A SET, REMOVE, ADD or DELETE whose path has a missing or wrong-typed parent is refused as an invalid document path. SET arithmetic and list_append return DynamoDB's errors for a missing or wrong-typed operand.

Scan validates the way the newer DynamoDB validation layer does, which 20 of 33 regions now run. That changes which error comes first and how it is worded for Limit, Segment, TotalSegments, TableName and Select, and an ExclusiveStartKey that doesn't match is answered with The provided starting key is invalid. Query and the multi-error messages for UpdateItem, BatchGetItem and DeleteItem move into DynamoDB's order too.

Two parallel Scan bugs are fixed. A TotalSegments above 4096 returns each item once, and a scan that deletes each page as it goes no longer stops early once the item its ExclusiveStartKey named has gone.

Changes to test assertions in 1.3.0

Set order now follows DynamoDB's response rules. String and binary sets sort by bytes. Number sets sort by value on GetItem, BatchGetItem, Query and item write ReturnValues, but use Java hash-set order on Scan, TransactGetItems, PartiQL, streams and transaction cancellation reasons. Compare sets as sets unless your test is checking that response behaviour. Old stored sets are sorted on HTTP, MCP and WASM responses; direct Rust reads retain their stored order until rewritten.

UpdateItem checks both the finished item's size and a charge based on what the update request writes. Untouched attributes no longer inflate the update charge, while arithmetic, if_not_exists, list_append and path-copy operands can exceed it even when the item barely grows. An over-limit charge is refused before evaluating the condition. Legacy AttributeUpdates uses the finished item alone; transaction updates and PartiQL UPDATE keep their previous sizing rules.

A SET at a list index past its end appends instead of padding with NULLs. Several such writes append in index order. Stepping through a missing list element or a NULL is refused as an invalid document path.

Validation messages and which error wins when a request has several problems also change to match captured AWS behaviour. Tests that expected a malformed request to succeed, or asserted Dynoxide's older error text, need updating. The changelog lists the individual cases.

Error codes

Errors carry DynamoDB-compatible codes with the com.amazonaws.dynamodb.v20120810# prefix: ResourceNotFoundException, ResourceInUseException, ValidationException, ConditionalCheckFailedException (with the item attached where DynamoDB attaches it), TransactionCanceledException, ItemCollectionSizeLimitExceededException, ProvisionedThroughputExceededException, LimitExceededException, DuplicateItemException, and InternalServerError.

Message text is matched to real DynamoDB wherever the conformance suite covers it, including the 1 validation error detected: envelope on the request-validation families that carry it and bare messages on the families that don't.

Not a production database

Dynoxide is built for local development, testing, and CI. It doesn't provide managed capacity, AWS load-based throttling, replication or IAM, and it isn't a hosted database. If you need any of that, you need real DynamoDB.