What your SDK can call, and what it can't.
AnimusDB speaks the DynamoDB wire protocol, so the AWS SDKs and the aws CLI work against it unmodified. Compatibility is not the same as identity, and this page is where the differences are written down rather than discovered.
A partitioned, sorted map of maps.
A wide-column key/value store in the Dynamo lineage. Tables hold items addressed by a partition key and an optional sort key. Attributes are schemaless: strings, numbers, binaries, booleans, nulls, lists, maps and sets.
If you have modelled for DynamoDB, you have modelled for this. Single-table designs, composite sort keys, sparse indexes and the rest all carry over, because they are properties of the API rather than of the service behind it.
Operation by operation.
| Operation | Status | Notes |
|---|---|---|
CreateTable | Supported | Partition and sort keys, GSIs and LSIs at creation, plus BillingMode (PROVISIONED or PAY_PER_REQUEST, the default) and, when PROVISIONED, ProvisionedThroughput. AttributeDefinitions must declare a type for every key attribute the table or an index uses, and nothing else — a missing or an unused definition is a ValidationException, matching DynamoDB |
DescribeTable | Supported | Includes index status and stream configuration |
UpdateTable | Supported | Add or drop a GSI on a populated table, with online backfill, toggle streams, or change BillingMode/ProvisionedThroughput — exactly one of those three changes per call. SSESpecification is rejected with a named ValidationException rather than silently ignored. ReplicaUpdates (with MultiRegionConsistency: STRONG, optionally a GlobalTableWitnessUpdates witness) converts an empty table into a multi-Region strongly consistent global table once the cluster version is finalized to 2; ReplicaUpdates Create/Delete with no consistency (or EVENTUAL) adds or removes an eventually consistent replica in a configured peer cluster, on a populated table, once the cluster version is finalized to 3, and, on an MRSC table, ReplicaUpdates Delete of two Regions (or of one replica plus the witness) once the cluster version is finalized to 7; on an eventually consistent table ReplicaUpdates Create/Update also accept a per-replica ProvisionedThroughputOverride (ReadCapacityUnits) on the table and per GSI, once the cluster version is finalized to 8; a table that is a legacy (2017.11.29) global table refuses ReplicaUpdates and throughput changes here (use the legacy operations); KMSMasterKeyId, OnDemandThroughputOverride and TableClassOverride are refused naming the missing feature; see Global tables below. Adding a GSI needs the same exact AttributeDefinitions coverage as CreateTable, scoped to that index's own keys |
PutItem · GetItem · DeleteItem | Supported | Condition expressions, projections, all attribute types |
UpdateItem | Supported | SET (literals, if_not_exists(), list_append(), and +/- numeric arithmetic), REMOVE, ADD, DELETE, all targeting nested document paths (a.b, a[0]) as well as top-level attributes, plus condition expressions |
Query | Supported | Key conditions, filters, index queries, pagination |
Scan | Supported | Filters and pagination. Ordered by token — see the differences below |
ExecuteStatement | Supported | SELECT, INSERT, UPDATE, and DELETE. A SELECT's WHERE pinning the partition key with = (plus a sort-key comparator, BETWEEN, or begins_with) lowers onto Query; anything else lowers onto Scan with a filter — the same key/filter split those two operations already make. INSERT/UPDATE/DELETE lower onto PutItem/UpdateItem/DeleteItem and run through the identical write path (conditions, index maintenance, streams, throttling all inherited): INSERT fails with DuplicateItemException on an existing key unless ON CONFLICT DO NOTHING is given; UPDATE fails on a missing item; DELETE of a missing key is a silent success; UPDATE/DELETE support RETURNING ALL OLD */ALL NEW *. Values must be ? placeholders bound via Parameters — never string-interpolated literals — though a document/list structure (nested {..}/[..]) may be written directly around those placeholders. NextToken (SELECT only) is opaque and bound to its own statement text |
BatchExecuteStatement | Supported | 1 to 25 statements, each the same SELECT/INSERT/UPDATE/DELETE grammar ExecuteStatement parses, run independently — no cross-statement atomicity, matching BatchWriteItem/BatchGetItem's own per-request contract. A batch SELECT must be an exact-key read (partition-key equality, plus sort-key equality on a composite table); a range or filtered SELECT is a per-statement error rather than a whole-request failure. One response entry per statement, in order, each carrying its own item or its own error code (ConditionalCheckFailed, DuplicateItem, ResourceNotFound, ValidationError, AccessDenied, ...) — a failing statement never blocks the rest of the batch |
ExecuteTransaction | Supported | 1 to 25 statements, parsed with the same PartiQL grammar as ExecuteStatement. All-SELECT runs as one TransactGetItems read (each statement must be an exact-key read — partition key, plus sort key on a composite table, nothing else: no index, no ORDER BY, no other WHERE term); all-INSERT/UPDATE/DELETE runs as one atomic TransactWriteItems write, with ClientRequestToken idempotency and per-statement CancellationReasons on a failed condition. Mixing SELECT with a mutation is rejected — a transaction is all-reads or all-writes. A transaction statement's own RETURNING clause is rejected (a successful TransactWriteItems action reports no item image); its wire-level ReturnValuesOnConditionCheckFailure is honored |
BatchWriteItem · BatchGetItem | Supported | Per-request atomicity, DynamoDB-faithful |
TransactWriteItems · TransactGetItems | Supported | Put, Update, Delete and ConditionCheck actions; ClientRequestToken idempotency on writes |
UpdateTimeToLive · DescribeTimeToLive | Supported | — |
TagResource · UntagResource · ListTagsOfResource | Supported | Wire metadata only — tags aren't consumed by placement, billing or anything else |
DescribeLimits · DescribeEndpoints | Supported | Static account/table ceilings and this node's own endpoint address — some SDK tooling probes these before doing anything else |
| Streams API | Supported | ListStreams, DescribeStream, GetShardIterator, GetRecords |
DeleteTable · ListTables | Supported | On the DynamoDB endpoint; also in the admin API and the consoles |
| Authentication (SigV4) | Opt-in | Off by default. Without it, any caller that can reach the port is trusted. When on, credentials are replicated cluster-wide, rotatable with a grace window, and each key can be scoped to a per-table/per-operation-class allow list — a static key file is a bootstrap source only |
| On-demand backup and restore | Supported | CreateBackup/DescribeBackup/ListBackups/DeleteBackup/RestoreTableFromBackup — a backup outlives a dropped source table; restore always creates a new table |
| Point-in-time recovery (PITR) | Supported | UpdateContinuousBackups/DescribeContinuousBackups/RestoreTableToPointInTime — restore to any second within the continuous-coverage window (also a new table; also works on a table since dropped) |
| Export to S3 | Supported | ExportTableToPointInTime/DescribeExport/ListExports — writes DynamoDB's own JSON export layout into a customer-named S3 bucket. Only ExportFormat: DYNAMODB_JSON and a full (not incremental) export are accepted today; requesting Ion format or an incremental export is a ValidationException. An ExportTime is validated against the table's PITR restore window but always exports current data, not a true point-in-time snapshot |
| Import from S3 | Supported | ImportTable/DescribeImport/ListImports — reads DynamoDB's own JSON export layout (this adapter's own export above, or real AWS's identical one) from a customer-named S3 bucket into a brand-new table, created from the request's own TableCreationParameters. Only InputFormat: DYNAMODB_JSON with GZIP or NONE compression are accepted today; requesting Ion/CSV format or Zstandard compression is a ValidationException. A malformed item (missing or mistyped key attributes) is skipped and counted, up to a bounded cap past which the import fails; a failed import rolls back the table it was creating, matching real DynamoDB. No CancelImport yet |
| Provisioned throughput / throttling | Supported | A table's declared ProvisionedThroughput, or a cluster-wide default when a table declares none, is enforced as a real per-tablet admission-control budget in DynamoDB capacity units — an exhausted budget returns ProvisionedThroughputExceededException (or, for TransactWriteItems, a ThrottlingError cancellation reason), exactly like real DynamoDB's own client-retry contract. Declaring throughput also sizes a table's tablet count up front, DynamoDB's own ceil(RCU/3000 + WCU/1000) formula. This gates admission, not spend — see the capacity card below for what's still out of scope. A transient unavailability that outlasts the server's own retry budget (a tablet split cutover, not a capacity refusal) returns ServiceUnavailable (HTTP 503) instead, the code every AWS SDK's default policy already retries with backoff. The same ServiceUnavailable is what a node returns when it sheds load: a connection beyond --max-connections (default 4096) or a request beyond --max-inflight (default 2048) is refused immediately, never queued |
| Global tables | Partial | Two modes, verified in deterministic simulation and, for MREC, by real-process tests. Multi-Region eventually consistent (MREC), the AWS default: independent clusters, each configured with its own region and its peers (cluster_settings; mutual TLS on the intra port; the Kubernetes operator renders them from spec.region/spec.peers, unit-tested and exercised by a two-cluster kind end-to-end test in CI), replicate a table asynchronously with last-writer-wins on a per-item timestamp; UpdateTable ReplicaUpdates Create/Delete (MultiRegionConsistency absent or EVENTUAL) works on a populated table, DescribeTable reports Replicas with a status, per-replica read-capacity overrides (ProvisionedThroughputOverride on the table and on each GSI, via ReplicaUpdates Create or Update; write capacity stays synchronized) are replicated to every cluster, reported in Replicas and enforced by the Region they name, from cluster version 8; they need a PROVISIONED table, and on-demand maximum throughput, table classes and KMS keys are refused because those table features do not exist; MRSC refuses all per-replica overrides (one stretched cluster shares one read budget per tablet), TTL settings follow, streams are forced to NEW_AND_OLD_IMAGES and a replica's stream sees the latest state of a key, not necessarily every intermediate write. Needs animus cluster finalize to cluster version 3 on every cluster. Real-process tests run two clusters over mutual TLS on loopback, one of them through an emulated 80 ms round-trip link (replication lag of about half a second at the median in that setup); there is no measurement from a real multi-region deployment and no cost figure. The AWS field names, shapes and status values were checked against the AWS SDK service model; error message texts are AnimusDB's own wording and cannot be checked without an AWS account. Multi-Region strongly consistent (MRSC) on one stretched cluster whose nodes carry topology.kubernetes.io/region labels (design: ADR 0075). UpdateTable with MultiRegionConsistency: STRONG and ReplicaUpdates Create (exactly three Regions: two Creates, or one plus a GlobalTableWitnessUpdates witness) converts an empty table; DescribeTable then reports GlobalTableVersion 2019.11.21, Replicas, MultiRegionConsistency and GlobalTableWitnesses. Matching AWS, MRSC does not support adding replicas to an existing table, per-replica overrides, TTL, local secondary indexes or the transaction APIs on such a table, and a single replica or the witness cannot be deleted. UpdateTable with ReplicaUpdates Delete naming two Regions, or one replica Delete plus a GlobalTableWitnessUpdates Delete of the witness, reverts the table to a regular single-Region table in the remaining replica's Region (the stretch cluster re-places every replica of the table into that Region and the MRSC restrictions stop applying to it); that needs animus cluster finalize to cluster version 7. The legacy 2017.11.29 operations (CreateGlobalTable, UpdateGlobalTable, DescribeGlobalTable, ListGlobalTables, DescribeGlobalTableSettings, UpdateGlobalTableSettings) work on top of MREC from cluster version 9 (animus cluster finalize): create an empty table with a NEW_AND_OLD_IMAGES stream in every Region first, then call CreateGlobalTable on any one cluster, which verifies the others before changing anything. Replication, last-writer-wins and the per-Region read-capacity overrides are MREC's; billing mode and write capacity are replicated; auto scaling, table classes and per-GSI write capacity are refused. The exception types are the ones the AWS model lists, but AWS's exact messages, and what it answers when a legacy operation meets a 2019.11.21 table, are unverified. Before version 9 they are refused by name. Zone-aware replica placement inside one cluster is available today on Kubernetes |
| On-demand billing metering | Out of scope | A property of a managed service, not of the API. "On-demand billing" here means the invoice, not the request — a table declaring no ProvisionedThroughput is genuinely unthrottled (PAY_PER_REQUEST), it just isn't metered or billed |
AWS-faithful, compiled in, no escape hatch.
DynamoDB's service limits are part of the API, not an operational afterthought — code that depends on ValidationException at 400 KB, or on BatchGetItem handing back UnprocessedKeys past 16 MB, needs that behavior to actually happen against a local cluster, not just against AWS. So every limit below is AWS's own published number, checked in the wire adapter itself. There is no config flag, environment variable or "unleashed" mode that raises or removes any of them — the ones marked Enforced below are load-bearing, not aspirational.
| Limit | Value | Status | Behavior |
|---|---|---|---|
| Item size | 400 KB | Enforced | Sum of every attribute's name plus its value, DynamoDB's own formula — checked on every write (PutItem, UpdateItem's post-update result, BatchWriteItem's PutRequests, TransactWriteItems' Put actions). ValidationException over the cap |
| Partition key value | 2048 B | Enforced | A table's own partition key, or a GSI's/LSI's |
| Sort key value | 1024 B | Enforced | A table's own sort key, or a GSI's/LSI's |
| Key attribute name | 255 chars | Enforced | The name of any attribute used as a partition or sort key |
| Attribute name | 1–64 KB | Enforced | Any attribute name, key or not; empty is never valid |
| Document nesting depth | 32 levels | Enforced | Nested List/Map depth within a single item |
| Expression string length | 4096 B | Enforced | Checked independently per expression on a request — ConditionExpression, UpdateExpression, ProjectionExpression, FilterExpression, KeyConditionExpression |
| Table / index name | 3–255 chars | Enforced | ASCII [a-zA-Z0-9_.-] only, matching AWS's own naming rule |
| GSIs per table | 20 | Enforced | At CreateTable and cumulatively across every UpdateTable that adds one |
| LSIs per table | 5 | Enforced | Declared at CreateTable only — LSIs can't be added later, matching AWS |
| Table provisioned throughput | 40,000 RCU / 40,000 WCU | Enforced | A single table's own declared ProvisionedThroughput, at CreateTable and UpdateTable |
BatchWriteItem request items | 25 | Enforced | Summed across every table named in RequestItems |
BatchGetItem request keys | 100 | Enforced | Summed across every table named in RequestItems |
TransactWriteItems actions | 100 | Enforced | Put/Update/Delete/ConditionCheck, combined |
TransactGetItems items | 100 | Enforced | — |
BatchExecuteStatement statements | 25 | Enforced | Zero statements is also rejected |
ExecuteTransaction statements | 25 | Enforced | Distinct from TransactWriteItems'/TransactGetItems' own 100-item cap; zero statements is also rejected |
Query/Scan evaluated page | 1 MB | Enforced | Per-page evaluated-item bytes, before any FilterExpression — the page is truncated with a LastEvaluatedKey, exactly like real DynamoDB's own well-known "fewer Items than Limit, still not done" surprise |
BatchGetItem response size | 16 MB | Enforced | Not an error: items past the budget, and every key not yet fetched, come back in UnprocessedKeys instead — same shape as a per-key throttle refusal |
BatchWriteItem request size | 16 MB | Enforced | Sum of every PutRequest item and DeleteRequest key. In practice never reachable through this adapter today — 25 items × the 400 KB item cap tops out at 10 MB — checked anyway so it's in place the moment either of those two caps is ever raised |
TransactWriteItems / TransactGetItems aggregate size | 4 MB | Enforced | Sum of every action's item (Put) or key (Update/Delete/ConditionCheck/Get) in the call — for a write, checked against the request; for a read, DynamoDB's own rule is on the response, so it's checked against what was actually fetched. Both fail the whole call with ValidationException, matching DynamoDB's all-or-nothing transaction contract |
Not yet enforced, and not expected to matter for most workloads: the Streams ShardIterator's 15-minute expiry, the LSI item-collection size limit (10 GB per partition key, counting the base item and every LSI projection sharing it), the aggregate size rules DynamoDB places on ExpressionAttributeNames/ExpressionAttributeValues, and the account-level 80,000 RCU/WCU ceiling — DescribeLimits reports that last one honestly as a static number, but nothing sums capacity across tables to check against it, because there's no account concept here to sum across.
Four things to check before you assume a drop-in.
Scans are ordered by token
A full Scan returns items in token order, not partition-key order. Sort-key ordering within a partition is preserved. If your code relies on the order a scan happens to produce, it was already relying on something DynamoDB does not promise — but it will now notice.
Throttling exists; billing doesn't
Declare a table's ProvisionedThroughput (or set a cluster-wide default) and it is enforced for real — ProvisionedThroughputExceededException and all. What's still missing is the meter: no invoice, no auto-scaling, no on-demand billing mode's own capacity-shaping behavior. Leave a table's throughput unset and it stays genuinely unthrottled, limited only by the hardware you gave it.
Credential checks are opt-in
SigV4 verification exists on the client port and is off by default. Until you switch it on, any value works. Switched on, a key can be scoped to specific tables and read/write/DDL/streams/backup classes — but this is a flat allow list, not IAM: condition keys, temporary credentials, and per-item or attribute-level authorization all have to be enforced somewhere else.
The AWS-side integrations aren't there
Streams work, but they aren't Lambda triggers. Global tables are available in both modes, and through the legacy 2017.11.29 operations, proven in simulation and, for MREC, on loopback with an emulated WAN link (no real multi-region deployment measurement; wire names checked against the AWS SDK model, error message texts not), and there is no DAX, no CloudWatch. Where a design leans on one of those, that part is a port rather than a config change.
--endpoint-url and run it. That takes an afternoon and it is worth more than this table.