> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tensor9.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Debugging AWS DynamoDB

> Diagnose table, key, condition, query, and backend problems without replaying uncertain writes.

Start with the operation the application sent, the response it received, and the table it addressed. Record the adapter and SDK versions, AWS account and signing Region, origin table name, selected target profile, and the installed configuration revision. The [DynamoDB service profile](https://docs.tensor9.com/cloud-adapter-early-access/service-catalog/aws/databases-storage/dynamodb-table.md) lists support by backend; [DynamoDB tuning](https://docs.tensor9.com/cloud-adapter-early-access/tuning/aws-dynamodb.md) identifies which capacity settings belong to a table, adapter, or shared target.

## Match the symptom to one check

| Symptom | First check | What it can establish |
| - | - | - |
| `ResourceNotFoundException` | `DescribeTable` using the same endpoint, Region, account, and table name | Whether that table exists in the selected adapter's registry; a similarly named native table is not sufficient. |
| `ValidationException` on a key or expression | Compare the request's attribute names and DynamoDB types with `KeySchema` and `AttributeDefinitions` | Whether the request names the correct partition and optional sort key. Check the selected profile for expression limits. |
| `ConditionalCheckFailedException` | Preserve the failed request's condition and read the exact item key | The error records a refused condition; the later read shows current state, not the value at evaluation time. It does not, by itself, show a capacity shortage. |
| Transaction rejection | Check every action, item identity, condition, and the backend's transaction support | A supported backend may still reject a request shape; the provisioned and serverless Cosmos profiles reject DynamoDB multi-item transaction APIs. |
| Empty or incomplete `Query` | Check `IndexName`, key condition, consistency flag, filter, `Limit`, and `LastEvaluatedKey` | An empty page can still have a continuation key when filtering removed the page's read items. |
| Slow request or throttling | Compare retries, target capacity, hot keys, index maintenance, and adapter connection use | Whether the bottleneck is at the target, adapter, or client. Increasing a DynamoDB billing value does not resize every target backend. |

Keep the raw service error code and request ID. A transport or SigV4 failure occurs before an adapter decision can be inferred. A validation or conditional error is an origin-facing result; a report can add recorded decisions but does not turn the failed operation into a success. A report receipt means an explanation was admitted, not that the operation or later work completed.

## Begin with a non-mutating table and item check

Use the installed **DynamoDB origin endpoint**, signing Region, and existing IAM workload profile for the same application identity. Set these placeholders from that install and client configuration; `DYNAMODB_ENDPOINT_URL` is distinct from the report service's `T9_ENDPOINT_URL`. Replace the example key with the actual table's complete key. Do not use a different Region or account to make a failed lookup appear successful.

```bash theme={null}
export DYNAMODB_ENDPOINT_URL="<published DynamoDB origin endpoint>"
export ORIGIN_AWS_REGION="<application signing Region>"
export ORIGIN_AWS_PROFILE="<existing application IAM workload profile>"
export TABLE_NAME="<exact table name>"

aws dynamodb describe-table \
  --endpoint-url "$DYNAMODB_ENDPOINT_URL" \
  --region "$ORIGIN_AWS_REGION" --profile "$ORIGIN_AWS_PROFILE" \
  --table-name "$TABLE_NAME"

aws dynamodb get-item \
  --endpoint-url "$DYNAMODB_ENDPOINT_URL" \
  --region "$ORIGIN_AWS_REGION" --profile "$ORIGIN_AWS_PROFILE" \
  --table-name "$TABLE_NAME" \
  --key '{"tenant_id":{"S":"tenant-001"},"order_id":{"S":"order-001"}}' \
  --consistent-read
```

The key example applies only to a table with those two string key attributes. Read `DescribeTable` first and construct a key from its actual schema. These ordinary metadata and item reads still follow the adapter's configured Explain default; a `409 ExplainedNotPerformed` preview is not evidence that the resource is missing. For a missing item, an empty `GetItem` result is different from a missing table. `ConsistentRead` is meaningful only where the selected mapping supports that read mode. AWS allows strong reads on tables and local secondary indexes, but not on global secondary indexes; target mappings can make different documented choices. Consult the exact [service profile](https://docs.tensor9.com/cloud-adapter-early-access/service-catalog/aws/databases-storage/dynamodb-table.md) before attributing a read difference to propagation. [AWS read consistency](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadConsistency.html) explains the origin contract.

For `Query`, preserve the original `LastEvaluatedKey` and pass it as `ExclusiveStartKey` on the next request with the same key condition. Do not treat an empty filtered page as the end of the result. Check whether the named index was declared and deployed for this backend. A local index, a global index, and a base-table query can take different target paths. [AWS pagination](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.Pagination.html) describes the continuation behavior; the service profile describes each mapping's index support.

## Worked case: a conditional update that times out

Suppose an order update sets `version` from `4` to `5` when `version = 4`. The SDK times out after sending `UpdateItem`. The timeout does not tell you whether the write committed. Save the request ID, exact table and key, condition, endpoint, Region, and report receipt if one was returned. Read that one item with the supported consistency option. If it now has version `5`, investigate whether this update or another writer made the change before deciding what to retry. If it remains `4`, the original request may still be unresolved; use the application’s idempotency and reconciliation rules. Never send an unconditional replacement just to produce a report.

If the SDK received `ConditionalCheckFailedException`, preserve the exact submitted condition and investigate competing writers. The item read afterward shows only its current value; it cannot reconstruct the condition's inputs or item value at the earlier evaluation time. If a transaction was involved, keep the `ClientRequestToken` and the exact action list. AWS documents a bounded idempotency window for `TransactWriteItems`; the selected adapter's transaction behavior must also be checked in its profile. [AWS transaction semantics](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/transaction-apis.html) explain the origin token.

To understand the request before another execution, use a signed explicit `Explain` control on an isolated, representative request, after checking support for that operation and installed version. A completed preview returns `409 ExplainedNotPerformed` and a receipt; it does not update the item. Unsupported capture can return a refusal instead. `ExecuteAndExplain` and the shorthand `x-t9-explain: true` execute the operation. Preserve the native SDK response and retrieve the report separately with signed `T9CloudAdapter.GetExplanation` on the install's published `t9` endpoint. Use signing service `t9`, the published signing Region, and the original report owner's IAM credentials, including the session token. A different assumed-role session cannot read that owner's report. See [Explaining SDK Requests](https://docs.tensor9.com/cloud-adapter-early-access/debugging/explain-sdk.md) and [Reading an Explanation](https://docs.tensor9.com/cloud-adapter-early-access/debugging/reading-explanations.md).

An explanation may show an adapter validation decision, a target operation attempt, an observed state, or a declared gap. Read its assurance and lifecycle before drawing a conclusion. PostgreSQL plans are [planned, operation-specific evidence](https://docs.tensor9.com/cloud-adapter-early-access/debugging/postgres-query-plans.md): a full plan may include physical relation names and bound constants, is visible only to the report owner, and estimates a separate committed-state observation. It does not prove that this update committed or that the native execution used that exact plan. Keep it with the report under your normal customer-data controls.

## Worked comparison: the same item on PostgreSQL and Cosmos DB

Suppose `GetItem` for the `orders` table is slow. The origin request and response can look identical across backends, while the target work differs. Check the installed table mapping first: the catalog includes PostgreSQL profiles, Google Cloud SQL for PostgreSQL, Azure Cosmos DB provisioned and serverless profiles, Firestore, Spanner and Bigtable. Operation support and evidence coverage remain specific to the selected profile and adapter version.

> [!NOTE]
>
> **PostgreSQL plan reporting is planned, October 25, 2026. Owner: Tensor9 Cloud Adapter team.** The examples below explain the reporting contract and the operation paths being qualified; they do not establish availability in your installed release. The paths being qualified include the `GetItem` keyed lookup, `PutItem` upsert, filtered, paginated `Query` and `Scan` on the base table and a declared secondary index, and the narrow performed `UpdateItem` path described below. Transaction-read plans are identified per requested member. These examples do not establish coverage for every update shape, transaction writes, every index, or every statement within a conditional write. A missing or unavailable fact must remain explicit.

### Retrieve the report from the request you already made

Keep the native DynamoDB result and the receipt from that same attempt. A successful `GetItem` still returns its ordinary `Item`; a completed explicit preview returns `409 ExplainedNotPerformed` without reading the item through the native execution path. Neither response contains a PostgreSQL plan.

Use the [signed SDK retrieval flow](https://docs.tensor9.com/cloud-adapter-early-access/debugging/explain-sdk.md#retrieve-without-replaying) to call `T9CloudAdapter.GetExplanation` with the receipt's report ID and `fmt: "Json"`. Sign for service `t9` using the published `T9_ENDPOINT_URL`, `T9_SIGNING_REGION` and the same report owner's IAM credentials, including any session token. Decode the outer AWS JSON response's `body` string, parse it as JSON and save the rendered report as `dynamodb-report.json`. Then inspect its PostgreSQL facts:

```bash theme={null}
jq '.facts[] | select(.assertion.type == "postgres-explain")' dynamodb-report.json
```

Retrieval does not replay `GetItem` or acknowledge the report. If collection is still in progress, poll the same report with bounded backoff; omit `minimumVersion` until you have a positive revision. Reads do not extend the fixed expiry in `x-t9-explain-expires-at`; the default retention is 24 hours.

### PostgreSQL: read the keyed lookup estimate

This is an **illustrative assertion excerpt**, not a captured production measurement or a complete report. Names, costs and values are example data. An actual fact preserves PostgreSQL's full JSON, including nested nodes and predicates.

```json theme={null}
{
  "assertion": {
    "factVersion": 2,
    "type": "postgres-explain",
    "payload": {
      "planningContext": "committed-state-observation",
      "statementCode": "dynamodb.get-item.select",
      "statementOrdinal": 1,
      "plan": [
        {
          "Plan": {
            "Node Type": "Index Scan",
            "Scan Direction": "Forward",
            "Index Name": "ddb_example_orders_pkey",
            "Relation Name": "ddb_example_orders",
            "Alias": "ddb_example_orders",
            "Startup Cost": 0.29,
            "Total Cost": 8.31,
            "Plan Rows": 1,
            "Plan Width": 64,
            "Index Cond": "((pk = 'S:tenant-001'::text) AND (sk = 'S:order-001'::text))"
          }
        }
      ]
    }
  }
}
```

The instrumented statement selects `item` by the physical `pk` and `sk`. The example index condition matches both encoded string keys; the physical relation and index names need not match `orders`. `Plan Rows: 1` is an estimate of rows emitted by this node, not proof that the item exists. `Total Cost: 8.31` is a PostgreSQL planner cost, not milliseconds. Compare the index condition with any residual `Filter` and nested `Plans`; a `Seq Scan` can be reasonable on a small table and is not automatically a defect.

For the initial `PutItem` path, look for `statementCode: "dynamodb.put-item.upsert"` and `statementOrdinal: 2`. It describes the keyed `INSERT ... ON CONFLICT ... DO UPDATE` statement. A plan can contain a `ModifyTable` root and its input node; estimated rows at a write node do not count committed items. An `ExecuteAndExplain` response and a separate item read establish the write result. A preview plan does not perform that insert or update.

For a performed `UpdateItem` that returns `ALL_NEW`, an installed adapter version that supports this capture can report two reached statements for a narrow case: complete string keys, no condition, no secondary indexes or enabled stream, and one or more `SET` assignments of string values to non-key attributes. Match `dynamodb.update-item.lock-read` at ordinal `1` to the locking `SELECT`, and `dynamodb.update-item.upsert` at ordinal `2` to the final replacement upsert. Keep both full JSON plans with the same operation; the native `Attributes` post-image and an independent item read establish the result. These are separate committed-state estimates, not the original transaction snapshot or evidence of a second write. Explicit `Explain` does not perform that update or supply this performed statement pair. Other return modes, conditions, action types, indexes and streams require their own statement evidence.

For a `Query` that supplies equality conditions for both attributes of a table's primary key, compare the key predicate with the filter separately. For example, suppose `orders` has string keys `pk` and `sk`, and the selected item has `note: "keep"`:

```json theme={null}
{
  "TableName": "orders",
  "KeyConditionExpression": "pk = :pk AND sk = :sk",
  "FilterExpression": "note = :note",
  "ExpressionAttributeValues": {
    ":pk": {"S": "tenant-001"},
    ":sk": {"S": "order-001"},
    ":note": {"S": "keep"}
  },
  "Limit": 1
}
```

When this complete-key statement is supported for plan capture, identify its `dynamodb.query-key.select` fact at ordinal `1`. Inspect the full PostgreSQL JSON for the physical relation, key predicate, scalar `note` comparison and nested nodes. An index condition and a residual `Filter` serve different roles; the planner may choose a different node shape as data and statistics change. Keep the complete JSON rather than extracting only a node name or estimated cost.

A false `note` filter and an absent key can both produce an empty native result. A plan estimate alone does not distinguish those outcomes or measure execution time. Compare the native `Items`, counts and continuation fields with the submitted key and filter, and keep the report's committed-state limitation. This example concerns a complete base-table key and a supported scalar filter; partial-key pagination, indexes and application-side residual predicates require their own statement evidence.

`planningContext: "committed-state-observation"` means the observer planned the actual SQL and encoded parameters on a separate authenticated, read-only connection. It uses `EXPLAIN (FORMAT JSON)` without `ANALYZE`; it does not share the original transaction snapshot or report actual execution time. Early revisions can precede the plan, and an unsupported route or unavailable observer can produce a declared limitation instead. Do not treat absence as evidence that no SQL ran.

### PostgreSQL transaction reads: match plans to requested members

Suppose one `TransactGetItems` reads two orders and one audit item. The following is an illustrative request shape, not a captured request or a command to repeat an unresolved transaction. The tables use the complete string keys shown here:

```json theme={null}
{
  "TransactItems": [
    {"Get": {"TableName": "orders", "Key": {"pk": {"S": "tenant-001"}, "sk": {"S": "order-001"}}}},
    {"Get": {"TableName": "orders", "Key": {"pk": {"S": "tenant-001"}, "sk": {"S": "order-002"}}}},
    {"Get": {"TableName": "audit", "Key": {"pk": {"S": "tenant-001"}, "sk": {"S": "event-001"}}}}
  ]
}
```

For an installed version and PostgreSQL mapping that support this capture, retrieve the report from the performed request using the owner-authenticated workflow above. Within each existing transaction-read child step, match its zero-based `transaction-read.index` fact to the corresponding member of your submitted `TransactItems` array, then inspect that child's reached `dynamodb.transact-get.select` fact at statement ordinal `1`. Several member reads can share that code and ordinal; keep their child steps and input positions distinct even when their SQL shape matches.

Compare the native `Responses` entries in request order. A missing item can occupy a response entry without an `Item`; its keyed `SELECT` can still have a plan estimate. Neither the estimated row count nor the number of plan facts proves how many items were returned. Follow the report's capture assurance and limitations rather than expecting one retained plan for every possible member of a large request.

The native PostgreSQL reads use one transaction. The diagnostic plans remain separate committed-state observations, not that transaction's original snapshot or measured execution. Keep the full owner-visible JSON and nested predicates; no `ANALYZE` or business-request replay is needed. This member-read example does not establish plan coverage for transaction writes, condition checks or every statement involved in a transaction.

### PostgreSQL Streams: distinguish shard enumeration from record reads

Suppose two ordinary item writes have produced records in a streamed table, and you are checking which shards the stream exposes. Preserve the native `DescribeStream` response and its `Shards` list. In an installed version that supports this capture, an `ExecuteAndExplain` report identifies the reached shard-enumeration statement as `dynamodb.stream.describe` at ordinal `1`.

That statement selects distinct shard identifiers from the table's PostgreSQL outbox, orders them, and applies the native enumeration limit. Inspect its full JSON for the outbox relation, nested nodes, row estimates and costs. The plan estimates the enumeration; it does not contain the returned shard list or prove how many records a shard holds. Compare the native response separately, and do not assume a particular scan or aggregation node.

This is separate from `GetRecords` evidence: `dynamodb.stream.bounds` at ordinal `2` describes the surviving sequence bounds, while `dynamodb.stream.read` at ordinal `3` describes the reached record-page query. Require the appropriate fact from that request's own report; a describe plan cannot stand in for a bounds or page plan. Explicit `Explain` for `DescribeStream` remains a catalog-only preview and supplies no shard-enumeration SQL plan.

These plans retain the same owner-only full-JSON and committed-state limits described above. Collection uses a separate authenticated read-only connection with `EXPLAIN (FORMAT JSON)` and no `ANALYZE`; it does not reproduce the original snapshot or measure execution time. Early report revisions can precede a plan. A declared gap or missing fact does not prove that no native SQL ran.

### Cosmos DB: compare the item path, not a PostgreSQL index

On the provisioned or serverless Cosmos DB profile, the equivalent keyed lookup resolves the item to a Cosmos partition and document ID, then performs a point read. Inspect the report's recorded validation, route and operation evidence where available, and compare it with the native `Item` or error. For instrumented table dispatch, `backend.target-kind` identifies the selected backend; an unknown target or declared gap is not permission to infer a route.

There should be no PostgreSQL plan for that Cosmos item operation. PostgreSQL costs and `Index Cond` do not explain Cosmos request units or throttling. Compare the independently observed Cosmos request charge, throttling and partition behavior for the same interval; those target metrics are separate evidence, not fields promised in every Explain report. The provisioned and serverless profiles also have different index support: a DynamoDB GSI requires the provisioned mapping, while the serverless profile supports local indexes only. Return to the exact [backend comparison](https://docs.tensor9.com/cloud-adapter-early-access/service-catalog/aws/databases-storage/dynamodb-table.md) before changing a query or capacity setting.

A full PostgreSQL plan can contain application literals, physical relation names and predicates. It remains private to the exact report owner in Human, Agent and JSON formats. Review a copy before sharing it; removing the item payload from a support bundle does not sanitize values embedded in a plan.

## Preserve the comparison

When escalating, include the redacted request shape, native status and error, report ID and revision, table description, exact key and index identity, target backend, adapter version, and a separately observed item or target state. Keep credentials, complete item payloads, and signed headers out of the shared bundle. For report-read problems, follow [Troubleshooting Explanations](https://docs.tensor9.com/cloud-adapter-early-access/debugging/troubleshooting.md); for capacity changes, return to [DynamoDB tuning](https://docs.tensor9.com/cloud-adapter-early-access/tuning/aws-dynamodb.md).

Share the table/key, selected backend and reviewed report excerpt with a [Support Agent](https://docs.tensor9.com/cloud-adapter-early-access/support/agents.md) to compare the mapping and database evidence before proposing a repair.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.