DynamoDB
LocalEmu's DynamoDB runs on pure-Python Moto. No Java, no JVM, no external binary. A LocalEmu layer wraps Moto to add full DynamoDB Streams (INSERT / MODIFY / REMOVE records across all four view types), transaction pre and post-processing so streams capture the right NewImage and OldImage, Global Tables v1 and v2 with replica routing, TTL bookkeeping, error injection, and state persistence. 100% of the 57 documented DynamoDB operations are implemented (34 with custom LocalEmu code, 23 forwarded to Moto, 0 missing). DynamoDB Streams: 4 of 4 operations, all custom.
Operation-level breakdown: see the DynamoDB coverage matrix and the DynamoDB Streams coverage matrix.
Quick start
$ awsemu dynamodb create-table \
--table-name users \
--attribute-definitions AttributeName=id,AttributeType=S \
--key-schema AttributeName=id,KeyType=HASH \
--billing-mode PAY_PER_REQUEST
TableStatus: ACTIVE
$ awsemu dynamodb put-item \
--table-name users \
--item '{"id":{"S":"u1"},"name":{"S":"Alice"},"age":{"N":"30"}}'
$ awsemu dynamodb get-item \
--table-name users \
--key '{"id":{"S":"u1"}}'
{
"Item": {
"id": {"S": "u1"},
"name": {"S": "Alice"},
"age": {"N": "30"}
}
}
$ awsemu dynamodb update-item \
--table-name users \
--key '{"id":{"S":"u1"}}' \
--update-expression 'SET age = :a, #s = :st' \
--expression-attribute-names '{"#s":"status"}' \
--expression-attribute-values '{":a":{"N":"31"},":st":{"S":"premium"}}' The AWS CLI, boto3, the JavaScript and Go SDKs, Terraform, the AWS CDK, and Pulumi all connect with only the endpoint URL changed.
Architecture
The DynamoDB provider lives at services/dynamodb/provider.py. Requests follow this path:
- •Custom handler runs first. For
PutItem,UpdateItem,DeleteItem,BatchWriteItem, andTransactWriteItems, the LocalEmu layer rewrites the request to ask Moto forReturnValues=ALL_OLDso the stream record builder can populateOldImage. - •Forward to Moto.
forward_request()inprovider.pycalls into the moto-ext backend bundled insideservices/moto/. Moto evaluates the conditional expression, the update expression, key constraints, and so on. - •Stream records emit. After Moto returns, the LocalEmu layer builds the stream record from the old image (which Moto just returned) and the new image (which the request body contains) and forwards it through
EventForwarderto the DynamoDB Streams service. - •Global Tables routing.
_forward_request()swaps the context region to the primary region when the table is part of a global table, so the single backing copy receives the write.
State is held in services/dynamodb/models.py (table definitions, TTL specifications, backups, streaming destinations, global table replicas, tags). When PERSISTENCE=1 is set, the store is serialised and restored across LocalEmu restarts.
DynamoDB Streams (services/dynamodbstreams/) is 100% custom. Under the hood, stream records are stored on a Kinesis stream named __ddb_stream_<TableName>; the DynamoDB Streams API surface (DescribeStream, GetShardIterator, GetRecords, ListStreams) translates Kinesis shard IDs into DynamoDB shard IDs and shapes records as DynamoDB stream events. Callers see DynamoDB Streams; the Kinesis backing is an implementation detail.
Configuration
Four DynamoDB-specific environment variables. All others (region, account, gateway port) come from the global LocalEmu configuration.
| Variable | Default | Purpose |
|---|---|---|
DYNAMODB_ERROR_PROBABILITY | 0.0 | Inject random faults into all DynamoDB operations. Useful for chaos testing your retry logic. |
DYNAMODB_READ_ERROR_PROBABILITY | 0.0 | Inject faults only on read operations: GetItem, Query, Scan, BatchGetItem, TransactGetItems. |
DYNAMODB_WRITE_ERROR_PROBABILITY | 0.0 | Inject faults only on write operations: PutItem, UpdateItem, DeleteItem, BatchWriteItem, TransactWriteItems. |
DYNAMODB_REMOVE_EXPIRED_ITEMS | false | When true, a background worker sweeps items whose TTL attribute has elapsed. Off by default: items past their TTL stay readable until you enable the sweeper. |
Features supported
| Feature | Notes |
|---|---|
| Table CRUD | CreateTable, UpdateTable, DeleteTable, DescribeTable, ListTables. |
| Item CRUD | PutItem, GetItem, UpdateItem, DeleteItem with stream emission on each. |
| Batch operations | BatchWriteItem, BatchGetItem with the AWS-standard 25 and 100 item limits and proper unprocessed-items semantics. |
| Query and Scan | Key-condition expressions, filter expressions (LocalEmu rejects filter references to primary-key attributes the way real AWS does), projections, pagination. |
| Conditional expressions | attribute_exists, attribute_not_exists, attribute_type, begins_with, contains, size, comparison operators, boolean composition. Failed conditions raise ConditionalCheckFailedException. |
| Update expressions | SET, ADD, REMOVE, DELETE with ExpressionAttributeNames and ExpressionAttributeValues. |
| Global Secondary Indexes | Created and updated via CreateTable / UpdateTable, queryable with the index-name parameter on Query. |
| Local Secondary Indexes | Created at table creation time, queryable via Query with the index-name parameter. |
| Streams | All four view types: KEYS_ONLY, NEW_IMAGE, OLD_IMAGE, NEW_AND_OLD_IMAGES. See the next section. |
| Transactions | TransactWriteItems (Put, Update, Delete, ConditionCheck) and TransactGetItems. Failures roll the entire transaction back and raise TransactionCanceledException. |
| Tags | TagResource, UntagResource, ListTagsOfResource. Stored cross-region in the LocalEmu store. |
| Time-To-Live | TTL specification CRUD. Real background expiry is opt-in via DYNAMODB_REMOVE_EXPIRED_ITEMS=true. |
| Backups | CreateBackup, RestoreTableFromBackup, ListBackups, DescribeBackup, DeleteBackup. |
| Point-in-Time Recovery | DescribeContinuousBackups, UpdateContinuousBackups. |
| Global Tables (v1 + v2) | Replica metadata stored, requests routed to the primary region. Single backing copy. |
| Import / Export | ImportTable (S3 source), ExportTableToPointInTime (S3 destination). |
| Billing modes | Both PROVISIONED and PAY_PER_REQUEST. Swap with UpdateTable --billing-mode. |
| PartiQL | ExecuteStatement, BatchExecuteStatement, ExecuteTransaction. |
DynamoDB Streams
Streams emit on every successful write, with the record content shaped by the stream's view type:
| StreamViewType | Record contents |
|---|---|
KEYS_ONLY | Only the primary key attributes. |
NEW_IMAGE | Keys plus the item as it looks after the write. |
OLD_IMAGE | Keys plus the item as it looked before the write. |
NEW_AND_OLD_IMAGES | Keys plus both the before and after images. |
Each record carries an eventName of INSERT, MODIFY, or REMOVE, a monotonic SequenceNumber, and an ApproximateCreationDateTime. Shard iterators support TRIM_HORIZON, LATEST, AT_SEQUENCE_NUMBER, and AFTER_SEQUENCE_NUMBER.
$ awsemu dynamodb create-table \
--table-name events \
--attribute-definitions AttributeName=id,AttributeType=S \
--key-schema AttributeName=id,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES
$ STREAM_ARN=$(awsemu dynamodb describe-table --table-name events \
--query 'Table.LatestStreamArn' --output text)
$ awsemu dynamodb put-item --table-name events --item '{"id":{"S":"e1"},"v":{"N":"1"}}'
$ awsemu dynamodb update-item --table-name events --key '{"id":{"S":"e1"}}' \
--update-expression 'SET v = :v' --expression-attribute-values '{":v":{"N":"2"}}'
$ awsemu dynamodb delete-item --table-name events --key '{"id":{"S":"e1"}}'
# Three stream records emitted: INSERT, MODIFY, REMOVE
$ SHARD_ID=$(awsemu dynamodbstreams describe-stream --stream-arn $STREAM_ARN \
--query 'StreamDescription.Shards[0].ShardId' --output text)
$ ITER=$(awsemu dynamodbstreams get-shard-iterator \
--stream-arn $STREAM_ARN --shard-id $SHARD_ID \
--shard-iterator-type TRIM_HORIZON \
--query 'ShardIterator' --output text)
$ awsemu dynamodbstreams get-records --shard-iterator $ITER \
--query 'Records[].dynamodb.{event:Keys, view:StreamViewType}' Wiring a Lambda function to the stream uses the standard event-source-mapping API. LocalEmu's Lambda poller (services/lambda_/event_source_mapping/pollers/dynamodb_poller.py) reads records from the stream and invokes the function once per batch.
# Wire a Lambda function to the DynamoDB stream
$ awsemu lambda create-event-source-mapping \
--function-name on-event \
--event-source-arn $STREAM_ARN \
--starting-position TRIM_HORIZON \
--batch-size 10
UUID: 8a7b6c5d-...
State: Enabled
# Now PutItem / UpdateItem / DeleteItem on the table will invoke the Lambda
# with the stream batch as its event payload. Examples
Global Secondary Index with Query
$ awsemu dynamodb create-table \
--table-name orders \
--attribute-definitions \
AttributeName=order_id,AttributeType=S \
AttributeName=customer_id,AttributeType=S \
--key-schema AttributeName=order_id,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--global-secondary-indexes \
'IndexName=by-customer,KeySchema=[{AttributeName=customer_id,KeyType=HASH}],Projection={ProjectionType=ALL}'
# Query the GSI
$ awsemu dynamodb query \
--table-name orders \
--index-name by-customer \
--key-condition-expression 'customer_id = :c' \
--expression-attribute-values '{":c":{"S":"c-42"}}' TransactWriteItems with rollback on ConditionCheck
$ awsemu dynamodb transact-write-items --transact-items '[
{"Put": {"TableName":"orders","Item":{"order_id":{"S":"o1"},"total":{"N":"99"}}}},
{"Update": {"TableName":"users","Key":{"id":{"S":"u1"}},
"UpdateExpression":"ADD spend :v",
"ExpressionAttributeValues":{":v":{"N":"99"}}}},
{"ConditionCheck": {"TableName":"users","Key":{"id":{"S":"u1"}},
"ConditionExpression":"attribute_exists(id)"}}
]'
# If any leg fails (e.g. ConditionCheck returns false), the entire
# transaction rolls back and no items are written. Limits and defaults
The AWS-standard DynamoDB limits (400 KB max item size, 25 items per BatchWriteItem, 100 items per BatchGetItem, 20 GSI per table, 5 LSI per table, 25 items per transaction) are enforced by the Moto backend. LocalEmu adds the following:
| Limit | Value | Source |
|---|---|---|
| Internal Scan / Query paginator size | 100 | services/dynamodb/provider.py:202 |
| DynamoDB Streams shard iterator cache | 10,000 entries | services/dynamodbstreams/provider.py:59 |
| ConsumedCapacity per response | 5 units (hardcoded) | services/dynamodb/provider.py:2127 |
Known limitations
All 57 DynamoDB operations and all 4 DynamoDB Streams operations are implemented. Behavioural caveats:
- •TTL items are not auto-deleted by default. The TTL specification is stored and reported by
DescribeTimeToLive, but the background sweeper only runs whenDYNAMODB_REMOVE_EXPIRED_ITEMS=trueis set. - •Global Tables do not yet replicate stream events to replica regions. A write to the primary region emits one stream record; the same write does not also fan out to the replica region's stream.
provider.py:1020tracks the work. - •TransactWriteItems and BatchGetItem do not yet route to Global Tables. Both work fine against single-region tables, but a transaction that targets a global table's replica region behaves as a local write on that region only.
- •ConsumedCapacity is reported as a fixed 5 units. LocalEmu does not compute capacity from request size. If your test asserts on exact capacity consumption, treat 5 as the constant value.
- •PartiQL updates with streams enabled are slow. LocalEmu detects stream-affecting PartiQL writes by re-scanning the table; expect this to be noticeably slower than the equivalent
UpdateItem. Tracking issue inprovider.py:1579. - •DescribeStream does not honour the
shard_filterparameter. All shards are returned regardless.
The data plane semantics are Moto's: condition evaluation, update expressions, type coercion, key constraints, and batch unprocessed-items behaviour all follow Moto's implementation rather than re-implementing AWS DynamoDB. Where Moto and AWS diverge (extreme number-precision rounding, certain reserved-attribute-name error codes), LocalEmu inherits the divergence.