> ## 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 SQS

> Trace queue identity, visibility, receipt handles, polling, FIFO ordering, and dead-letter behavior.

Start with the complete queue URL and the operation, not just a queue name. Record the adapter and SDK versions, origin account and signing Region, selected Standard or FIFO profile, target backend, and configuration revision. Queue URLs identify where the SDK sends subsequent requests; a URL from another install or account can explain a missing queue without any message loss. Check [SQS Standard](https://docs.tensor9.com/cloud-adapter/service-catalog/aws/messaging-streaming/sqs-classic.md) or [SQS FIFO](https://docs.tensor9.com/cloud-adapter/service-catalog/aws/messaging-streaming/sqs-fifo.md) for the exact backend. [SQS tuning](https://docs.tensor9.com/cloud-adapter/tuning/aws-sqs.md) covers visibility, long polling, capacity, and backend differences.

## Check the queue without consuming a message

Use the installed **SQS origin endpoint**, signing Region, and existing IAM workload profile with the actual queue URL. Set these placeholders from the application configuration; `SQS_ENDPOINT_URL` is distinct from the report service's `T9_ENDPOINT_URL`.

```bash theme={null}
export SQS_ENDPOINT_URL="<published SQS origin endpoint>"
export ORIGIN_AWS_REGION="<application signing Region>"
export ORIGIN_AWS_PROFILE="<existing application IAM workload profile>"
export SQS_QUEUE_URL="<exact queue URL>"

aws sqs get-queue-attributes \
  --endpoint-url "$SQS_ENDPOINT_URL" \
  --region "$ORIGIN_AWS_REGION" --profile "$ORIGIN_AWS_PROFILE" \
  --queue-url "$SQS_QUEUE_URL" \
  --attribute-names QueueArn VisibilityTimeout ReceiveMessageWaitTimeSeconds \
    ApproximateNumberOfMessages ApproximateNumberOfMessagesNotVisible \
    ApproximateNumberOfMessagesDelayed RedrivePolicy FifoQueue
```

This is a non-mutating check. It still follows the adapter's configured Explain default; a `409 ExplainedNotPerformed` preview is not a queue-attribute result or evidence that the queue is missing. Some attributes are approximate, delayed, unavailable, or declared rather than live on particular mappings. Do not infer that a message is absent from one count. Compare the returned queue ARN, account, Region, queue type, and installed target profile before investigating delivery. A `ReceiveMessage` call is different: it can claim messages and start their visibility timeout, even if the consumer later crashes.

| Symptom | First check | Likely distinction |
| - | - | - |
| Queue not found or access denied | URL, endpoint, account, signing Region, policy, and selected profile | A valid URL in another install does not establish a queue here; origin IAM and target access are separate. |
| Empty receive after send | Delay, visible and in-flight counts, wait time, another consumer, and FIFO group | A delayed or currently claimed message need not appear in this receive. An empty response is not proof that the send failed. |
| Duplicate processing | Processing time, visibility expiry, delete result, and backend restart | Standard delivery is at least once. A worker must make its business action idempotent. |
| `ReceiptHandleIsInvalid` or stale-handle error | The handle from the latest receive and the selected backend's restart rule | A receipt handle belongs to one claim. Service Bus and Pub/Sub handles differ from durable PostgreSQL handles. |
| Slow or unexpectedly long receive | `WaitTimeSeconds`, queue default, and backend implementation | Some mappings poll; Pub/Sub uses StreamingPull; the FIFO Service Bus session accept can overrun the requested wait. |
| FIFO group stalls or send is suppressed | Group ID, deduplication ID, in-flight head, and backend echo behavior | One blocked group can coexist with progress in other groups. Service Bus has documented response differences. |
| Dead-letter count or route differs | Redrive policy, receive count, target DLQ identity, and backend support | Azure Queue Storage uses application-managed poison delivery; other backends implement different redrive paths. |

Read the batch response as well as its HTTP status. `SendMessageBatch` and `DeleteMessageBatch` can return per-entry failures after a successful HTTP response. Preserve each entry ID and error code. A service error remains the SDK's native SQS result; an explanation report records adapter decisions separately. Network or SigV4 failures may leave no report receipt.

## Worked case: a job reappears after a slow worker

A worker receives `invoice-1001`, starts a six-minute database task, and sees the same job again. First inspect the queue's `VisibilityTimeout` and the receive attempt's timestamp. If visibility was 120 seconds, a second receive after that interval is expected when the first worker has not deleted or extended the claim. Compare both receipt handles; delete or change visibility using the current handle, following that backend's rules. A Service Bus mapping caps requested visibility at five minutes and its broker-lock handles do not survive an adapter restart. PostgreSQL mappings store durable handles, but a stale claim still fails. Confirm the selected profile before changing the timeout.

If this is a FIFO queue, record `MessageGroupId` and `MessageDeduplicationId`. An in-flight head blocks later messages in its group while other groups can proceed. A repeated send with the same deduplication ID may be suppressed during the documented window; it is not proof that a consumer finished processing. Azure Service Bus has a documented difference in the ID returned for a suppressed send. [AWS FIFO terms](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-key-terms.html) explain the origin behavior; [the FIFO profile](https://docs.tensor9.com/cloud-adapter/service-catalog/aws/messaging-streaming/sqs-fifo.md) states mapping differences.

Do not send the job again to diagnose it. Check the application's durable job ID and downstream completion first. If the send response was lost, reconcile message and business state before deciding whether a new send is safe. [AWS visibility guidance](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_ReceiveMessage.html) explains why an unacknowledged message can become visible again.

## Use a report for a specific request

For a non-executing request check, confirm installed capture support for that operation, then add a signed version-1 `x-t9-explain` control with mode `Explain` before signing. A completed preview returns `409 ExplainedNotPerformed` with a receipt; unsupported capture can return a refusal. `ExecuteAndExplain` and `x-t9-explain: true` execute the SQS operation; a preview of `ReceiveMessage` must not be treated as a claim. Do not use an executing control to replay an uncertain send or delete.

Save the native status, SQS error code, request ID, `x-t9-explain-id`, and receipt expiry. Read the existing report with `T9CloudAdapter.GetExplanation` on the install's published `t9` endpoint, SigV4 service `t9`, published signing Region, and the same report owner's IAM credentials and session token. The developer CLI can [read an existing report](https://docs.tensor9.com/cloud-adapter/debugging/explain-sdk.md#retrieve-an-existing-report-with-t9dev) without rerunning the queue operation. A receipt does not prove that asynchronous target work finished; inspect report lifecycle and [follow asynchronous explanations](https://docs.tensor9.com/cloud-adapter/debugging/async-explanations.md) where applicable.

PostgreSQL-backed queues may eventually include [full PostgreSQL plan evidence](https://docs.tensor9.com/cloud-adapter/debugging/postgres-query-plans.md) for qualified operations. Availability depends on the installed version and backend. Plans can expose physical relation names and predicates, are owner-only, and describe a separate committed-state estimate. They do not prove a message was delivered, deleted, or dead-lettered. Use the queue attributes, exact message identity, native result, and downstream state for those conclusions.

## Worked comparison: send one job through different backends

Suppose the application sends `invoice-1001` once and receives a successful `SendMessage` response. Keep that native response's message ID and report receipt. The same SQS operation can map to a PostgreSQL insert, a Service Bus broker send, or a Pub/Sub publish. The [Standard](https://docs.tensor9.com/cloud-adapter/service-catalog/aws/messaging-streaming/sqs-classic.md) and [FIFO](https://docs.tensor9.com/cloud-adapter/service-catalog/aws/messaging-streaming/sqs-fifo.md) profiles identify which mapping is installed; a backend supported for Standard is not automatically supported for FIFO.

### Retrieve the existing send report

Use the report receipt from the original send, rather than sending the message again. Keep the origin SQS endpoint separate from the published `t9` report endpoint:

```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 SQS response>"

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

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

The CLI uses the same report owner's IAM credentials to sign `T9CloudAdapter.GetExplanation` for service `t9`. It prints the rendered report, sends no SQS operation and does not acknowledge the report. With a direct SDK client, first decode the outer AWS JSON response's `body` string, then inspect the rendered JSON's `facts`. A new assumed-role session cannot read the original session's report.

If collection is still in progress, read the same report with bounded backoff. Omit `minimumVersion` until a positive revision exists. Export any needed evidence before acknowledgment or the fixed expiry in `x-t9-explain-expires-at`; reads do not renew the default 24-hour retention. See [existing-report retrieval](https://docs.tensor9.com/cloud-adapter/debugging/explain-sdk.md#retrieve-an-existing-report-with-t9dev).

### PostgreSQL: inspect the insert estimate

> [!NOTE]
>
> **PostgreSQL plan reporting is planned, October 25, 2026. Owner: Tensor9 Cloud Adapter team.** The Standard PostgreSQL paths being qualified include `SendMessage`, `ReceiveMessage`, `DeleteMessage` and `ChangeMessageVisibility`. The example below focuses on statement code `sqs.message.insert` and ordinal `1`; it does not establish availability in every installed release. These paths do not establish plan support for FIFO deduplication, batch operations or dead-letter movement. Confirm the exact operation, backend and installed version before requesting capture.

This **illustrative assertion excerpt** uses example relation names and estimates. It is not an actual captured send or a complete report. An actual fact retains the full PostgreSQL JSON tree.

```json theme={null}
{
  "assertion": {
    "factVersion": 1,
    "type": "postgres-explain",
    "payload": {
      "planningContext": "committed-state-observation",
      "statementCode": "sqs.message.insert",
      "statementOrdinal": 1,
      "plan": [
        {
          "Plan": {
            "Node Type": "ModifyTable",
            "Operation": "Insert",
            "Relation Name": "sqs_example_invoice_jobs",
            "Alias": "sqs_example_invoice_jobs",
            "Startup Cost": 0.0,
            "Total Cost": 0.01,
            "Plan Rows": 1,
            "Plan Width": 48,
            "Plans": [
              {
                "Node Type": "Result",
                "Parent Relationship": "Outer",
                "Startup Cost": 0.0,
                "Total Cost": 0.01,
                "Plan Rows": 1,
                "Plan Width": 192
              }
            ]
          }
        }
      ]
    }
  }
}
```

The native Standard send inserts the encoded body, attributes, optional message group and delayed visibility time into the selected queue's physical table, then returns its identifiers. The example `ModifyTable` → `Result` tree estimates that insert. The relation name identifies a physical table, not necessarily the queue URL or origin name. `Plan Rows` estimates the output at each node; it does not count delivered messages. `Total Cost` uses PostgreSQL planner units, not milliseconds. Compare costs only for comparable statements, inputs and database contexts.

A direct values insert need not have an `Index Scan` or an `Index Cond`. That absence does not show a missing queue index or describe the later receive path. PostgreSQL receives use a different statement to select eligible rows and claim them with `FOR UPDATE SKIP LOCKED`; FIFO group selection and dead-letter routing add other work. A send plan does not qualify those statements or prove that a particular message was visible to a consumer. If a separately supported statement's plan includes an index condition, residual filter or nested scan, inspect those predicates and row estimates together rather than assuming the root node explains all work.

`planningContext: "committed-state-observation"` means the observer uses the actual compiled SQL and encoded parameters on a separate authenticated, read-only PostgreSQL connection. It runs `EXPLAIN (FORMAT JSON)` without `ANALYZE`; it does not insert a second message, measure execution time, or share the original send's transaction snapshot. The native result and independently observed message or business state establish effects. A complete plan may arrive in a later report revision; unsupported context or unavailable planning capacity must remain an explicit limitation.

### Service Bus and Pub/Sub: inspect broker evidence

For an Azure Service Bus queue, investigate the selected broker entity, send acknowledgment and the consumer's later lock handling. For a Google Cloud Pub/Sub Standard mapping, investigate publish acknowledgment, subscription delivery and acknowledgment behavior. These are actual supported backend profiles, but their availability does not imply every operation has complete Explain capture.

Read the report's recorded admission decisions, planned effects, observed outcomes and declared gaps for the captured operation. Use the native message ID and separately obtained broker or application observations to connect the send with processing. No PostgreSQL plan belongs to these message operations. A broker acknowledgment does not prove that a worker completed the invoice, and it does not turn broker timing or delivery state into a report field when that evidence was not recorded.

For the slow-worker case above, supported receive capture can record `sqs.receive.visibility-seconds`, `sqs.receive.visibility-source` and `sqs.receive.wait-seconds`. These describe selected settings; `sqs.receive.message-eligibility` can remain unknown. A Service Bus lock that expires or is lost across an adapter restart differs from a durable PostgreSQL receipt handle. A Pub/Sub subscription's acknowledgment behavior differs again. Compare the installed backend's rules and actual receive/delete outcomes before deciding to retry.

Full PostgreSQL plans remain private to the exact report owner in every format. They may contain physical relation names and application values inside predicates. Sanitize a copy before sharing it, even when the message body has already been removed from the support bundle. See [PostgreSQL plan interpretation and privacy](https://docs.tensor9.com/cloud-adapter/debugging/postgres-query-plans.md).

For a support handoff, include redacted queue URL and ARN, account and Region, Standard or FIFO profile, target backend, queue attributes, message/group/dedup IDs where relevant, receive and delete times, SDK error and attempt IDs, report revision and limitations, and independent business-state observation. Remove message bodies and credentials from the shared bundle. Use [Troubleshooting Explanations](https://docs.tensor9.com/cloud-adapter/debugging/troubleshooting.md) for missing receipts or report access failures.

A [Support Agent](https://docs.tensor9.com/cloud-adapter/support/agents.md) can help correlate the queue backend, claim timeline and reviewed report without treating a duplicate delivery as permission to replay a business action.


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