> ## 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/service-catalog/aws/databases-storage/dynamodb-table.md) lists support by backend; [DynamoDB tuning](https://docs.tensor9.com/cloud-adapter/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/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/debugging/explain-sdk.md) and [Reading an Explanation](https://docs.tensor9.com/cloud-adapter/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/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, and filtered, paginated `Query` and `Scan` on the base table and a declared secondary index. This does not establish plan coverage for `UpdateItem`, transactions, 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 install's published report endpoint and signing Region, with the **same report owner's IAM session** that made the captured request:

```bash theme={null}
export T9_ENDPOINT_URL="<published t9 service endpoint>"
export T9_SIGNING_REGION="<published t9 signing Region>"
export REPORT_ID="<x-t9-explain-id from that DynamoDB response>"

t9dev ext explain --report "$REPORT_ID" \
  --awsProfile "$ORIGIN_AWS_PROFILE" --fmt Json > dynamodb-report.json

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

The CLI signs `T9CloudAdapter.GetExplanation` for service `t9` and prints the rendered report. It does not replay `GetItem` or acknowledge the report. A direct SDK client instead decodes the outer AWS JSON response's `body` string before inspecting `facts`. 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. See [existing-report retrieval](https://docs.tensor9.com/cloud-adapter/debugging/explain-sdk.md#retrieve-an-existing-report-with-t9dev).

### 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": 1,
    "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.

`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.

### 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/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/debugging/troubleshooting.md); for capacity changes, return to [DynamoDB tuning](https://docs.tensor9.com/cloud-adapter/tuning/aws-dynamodb.md).

Share the table/key, selected backend and reviewed report excerpt with a [Support Agent](https://docs.tensor9.com/cloud-adapter/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.