# ChangeMessageVisibility: troubleshoot SQS message timeout errors

> ChangeMessageVisibility resets SQS message visibility. Use current receipt handles, bounded timeout extensions and per-entry batch error checks with Lambda.

By Boris Tane, Founder of Polylane · Published October 6, 2026 · 7 min read
Canonical: https://polylane.com/learn/troubleshooting/ChangeMessageVisibility/

ChangeMessageVisibility sets a new timeout for an in-flight SQS message using its receipt handle. The countdown starts when the change is made, replacing the old deadline; it does not reset the 12-hour maximum from ReceiveMessage. Use the latest handle, extend before expiry, and inspect per-entry failures for batch changes.

`ChangeMessageVisibility` changes how long an in-flight Amazon SQS message stays hidden from other consumers. The [API reference](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_ChangeMessageVisibility.html) requires a queue URL, the receipt handle from `ReceiveMessage`, and a timeout in seconds. Extend active work before visibility expires, or set zero to release it; neither operation deletes the message.

When a change fails, inspect the error and receipt-handle lifecycle before increasing the timeout. For Lambda consumers, first inspect the source queue and event source mapping rather than importing a standalone worker's heartbeat strategy.

## Meaning: a new countdown, not added time

A received SQS message is hidden for the queue's visibility timeout or the timeout supplied to `ReceiveMessage`. The queue default is 30 seconds. It stays in flight until deletion or the end of visibility.

`ChangeMessageVisibility` starts a new countdown immediately. AWS's [example](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_ChangeMessageVisibility.html) receives a message with a 60-second timeout, waits 15 seconds, then changes visibility to ten seconds. It becomes eligible for visibility at about second 25, not second 70. A shorter change can therefore release work earlier than the old deadline.

The valid duration range is 0 to 43,200 seconds. A per-message change is temporary; the next receive uses the queue's original setting unless that receive supplies an override. It does not permanently rewrite queue configuration or the message's default timeout.

There is also a [12-hour maximum from the ReceiveMessage request](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/best-practices-processing-messages-timely-manner.html). Renewals do not reset it. Asking for the full 43,200 seconds after the response arrives may already exceed remaining time because the receive consumed part of that budget. SQS fails the request rather than clamping it to what remains.

Visibility reduces concurrent delivery; it is not an exactly-once processing guarantee. Keep business operations idempotent even when every renewal returns successfully.

## Causes to test when a change fails

These are diagnostic possibilities, not prevalence claims:

- The worker supplied a message ID instead of a receipt handle, or reused a handle from an older receive.
- Visibility expired or another path deleted the message before the change reached SQS.
- The requested timeout exceeds remaining time in the 12-hour budget.
- The URL points to the wrong queue, account or Region, or the queue was deleted.
- Frequent heartbeat requests reached a request-rate limit.
- Authorization or request signing is wrong.
- A batch returned failed entries that the worker ignored because HTTP status was 200.

Capture queue ARN, message ID, receive time, requested duration, AWS request ID and exact error code. Avoid publishing receipt handles and message bodies in broad logs. They are not useful high-cardinality metric dimensions and can expose application data.

## Confirm queue configuration and handle ownership

Start with read operations in the intended account and Region:

```bash
export AWS_REGION=us-east-1
QUEUE_URL=https://sqs.us-east-1.amazonaws.com/123456789012/orders
aws sts get-caller-identity
aws sqs get-queue-attributes --queue-url "$QUEUE_URL" \
  --attribute-names QueueArn VisibilityTimeout RedrivePolicy \
    ApproximateNumberOfMessages ApproximateNumberOfMessagesNotVisible
```

In **SQS → Queues → your queue → Configuration**, inspect visibility timeout and redrive policy. In **Monitoring**, look at visible and in-flight backlog and message age. Those are queue-level indicators, not proof that a particular handle's renewal succeeded.

Use a disposable queue or known test message for reproduction. Receiving a live production message changes its visibility and can compete with real workers:

```bash
aws sqs receive-message --queue-url "$QUEUE_URL" \
  --max-number-of-messages 1 --wait-time-seconds 10 \
  --visibility-timeout 120

# Set this to ReceiptHandle from the response, not MessageId.
aws sqs change-message-visibility --queue-url "$QUEUE_URL" \
  --receipt-handle "$RECEIPT_HANDLE" --visibility-timeout 120
```

The successful response is HTTP 200 with an empty body, so the CLI may print nothing. It does not return a new receipt handle. Continue tracking the handle for that receive; a subsequent receive has its own handle.

## Exact errors and the next check

The [operation-specific error table](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_ChangeMessageVisibility.html#API_ChangeMessageVisibility_Errors) lists these errors with HTTP 400:

| Error | Meaning and confirmation |
| --- | --- |
| `MessageNotInflight` | Message is not in flight; inspect expiry, deletion and active ownership. |
| `ReceiptHandleIsInvalid` | Handle is invalid; check the latest receive and corresponding queue. |
| `QueueDoesNotExist` | Inspect exact URL, Region, account and deletion history. |
| `RequestThrottled` | Request rate exceeded permitted limits; inspect heartbeat rate and batching. |
| `InvalidSecurity` | Request lacked HTTPS or SigV4 signing; use the SDK/CLI signing path. |
| `InvalidAddress` | Specified address/ID is invalid; check the target. |
| `UnsupportedOperation` | Unsupported operation; inspect request and service response. |

Common authorization errors can occur as well. Preserve the actual SDK error instead of reducing every failure to "timeout." The reference says exceeding remaining visibility time returns an error but does not establish one universal named exception for every such request. Diagnose that case with elapsed time, requested duration and returned message.

Do not retry a permanently invalid or expired handle forever. Once the worker cannot establish a successful renewal, concurrent processing is possible. Stop or checkpoint where the application supports it, and use idempotency to prevent duplicated irreversible effects.

## Fix standalone consumers with bounded heartbeats

AWS's [timely-processing guidance](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/best-practices-processing-messages-timely-manner.html) suggests an initial two-minute visibility timeout and extensions every minute for work of uncertain length. It is an example, not a universal rate. Leave margin for latency and retries, measure elapsed time monotonically from receiving, and stop extending abandoned or completed jobs.

Boto3 uses these real renewal and deletion operations:

```python
import boto3
from botocore.config import Config

sqs = boto3.client(
    "sqs",
    config=Config(retries={"mode": "standard", "total_max_attempts": 4}),
)

def extend_message(queue_url, receipt_handle, seconds):
    # Caller checks remaining receive budget and keeps a safety margin.
    return sqs.change_message_visibility(
        QueueUrl=queue_url, ReceiptHandle=receipt_handle,
        VisibilityTimeout=seconds,
    )

def delete_completed_message(queue_url, receipt_handle):
    return sqs.delete_message(
        QueueUrl=queue_url, ReceiptHandle=receipt_handle,
    )
```

These helpers do not implement a complete scheduler. The worker must observe heartbeat failures, serialize renewal completion with shutdown/deletion and enforce an overall processing deadline. A background task that fails silently has not renewed anything.

Delete after the business operation completes durably. Repeatedly renewing a stuck job delays retry and holds in-flight capacity. For work longer than 12 hours, split/checkpoint the job or use another workflow such as Step Functions, as AWS recommends.

## Batch extensions: inspect every entry

[ChangeMessageVisibilityBatch](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_ChangeMessageVisibilityBatch.html) accepts at most ten entries, each with a distinct batch ID, current receipt handle and timeout. HTTP 200 can include both successful and unsuccessful entries:

```python
result = sqs.change_message_visibility_batch(
    QueueUrl=queue_url,
    Entries=[
        {"Id": "job-1", "ReceiptHandle": handle_one, "VisibilityTimeout": 120},
        {"Id": "job-2", "ReceiptHandle": handle_two, "VisibilityTimeout": 120},
    ],
)
for failure in result.get("Failed", []):
    print(failure["Id"], failure["Code"], failure["SenderFault"])
    # Retry or stop this entry according to its error and remaining budget.
```

Batch request errors include `BatchEntryIdsNotDistinct`, `EmptyBatchRequest`, `InvalidBatchEntryId` and `TooManyEntriesInBatchRequest`. Fix malformed input instead of retrying it unchanged. For transient failures, retry only appropriate failed entries and recompute remaining time. Do not reset a failed entry's local deadline because another entry succeeded.

## Lambda interplay: source queue settings first

Lambda polls SQS and invokes synchronous batches. Its [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-configure.html) recommends queue visibility of at least six times function timeout plus `MaximumBatchingWindowInSeconds` when a batch window is used. Lambda validates that function timeout is no greater than queue visibility when creating or updating a mapping.

A 30-second function timeout and five-second batch window give a recommended starting visibility of 185 seconds:

```bash
aws sqs set-queue-attributes --queue-url "$QUEUE_URL" \
  --attributes VisibilityTimeout=185
aws lambda list-event-source-mappings --function-name orders-worker
```

Calculate from the actual function and window. Inspect mapping state, batch size, concurrency and `FunctionResponseTypes`. Partial batch responses let Lambda retry only failed records instead of successful records from the same batch. Configure the DLQ on the SQS source queue; [AsyncEventsDropped](/learn/troubleshooting/AsyncEventsDropped/) belongs to Lambda's separate internal async invocation queue.

A handler that calls this API needs explicit `sqs:ChangeMessageVisibility` permission. The call does not extend Lambda execution time. Smaller work units and bounded downstream calls are better than preserving a message for a handler that cannot finish it.

## Verify recovery and prevent repeats

In a test queue, renew a received message, verify it remains unavailable during the intended window, then allow expiry and confirm later redelivery. Delete a completed test message and check the durable business result. Exercise a deliberately invalid batch entry so your failure handling is verified, not merely the happy path.

Track processing duration, renewal failures, receive counts and DLQ backlog. The [LambdaInsightsExtension guide](/learn/troubleshooting/LambdaInsightsExtension/) helps investigate function CPU and memory pressure. Keep [investigation reads separate from operational writes](/learn/ai-in-production/is-it-safe-to-give-an-ai-sre-read-access-to-production/) such as receives and renewals.

Polylane's [AWS integration](/integrations/aws/) connects it to AWS, so it can see when a queue backs up and work out why. The [AWS overview](/for/aws/) has more. Visibility deadlines and duplicate-handling guarantees still belong to the consumer design.

Running on AWS? See [how Polylane monitors AWS in production](https://polylane.com/for/aws/).

## Common questions

**Does ChangeMessageVisibility add time to the existing deadline?**

No. The requested duration starts from the change and replaces the current countdown. Setting 120 seconds means about 120 seconds from the change, not 120 seconds after the old deadline.

**Can I extend a message beyond 12 hours?**

No. The maximum is measured from when SQS receives the ReceiveMessage request. Extensions do not reset it, and a request beyond the remaining time fails rather than being automatically shortened.

**Does HTTP 200 mean every batch entry succeeded?**

No. ChangeMessageVisibilityBatch can return HTTP 200 with a non-empty Failed array. Inspect each entry's Id, Code, Message and SenderFault before treating that message as extended.

**Should a Lambda SQS handler extend messages automatically?**

Start with queue visibility and event source mapping configuration. AWS recommends at least six times the function timeout plus the batch window. An extension cannot make the handler run past its function timeout.

**Does setting visibility to zero delete the message?**

No. It ends current visibility so the message can be received again. DeleteMessage is a separate operation used after processing completes durably.

## Sources

- [ChangeMessageVisibility API reference](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_ChangeMessageVisibility.html)
- [ChangeMessageVisibilityBatch API reference](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_ChangeMessageVisibilityBatch.html)
- [Timely processing and the visibility limit](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/best-practices-processing-messages-timely-manner.html)
- [Lambda SQS mapping configuration](https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-configure.html)
- [Polylane AWS integration](https://docs.polylane.com/integrations/aws)

## About the author

Boris Tane is the founder of Polylane. He previously founded Baselime, observability for the future of the cloud, which Cloudflare acquired. At Cloudflare he built and led the Workers observability team.

## Related

- [AsyncEventsDropped: diagnose Lambda async event loss](https://polylane.com/learn/troubleshooting/AsyncEventsDropped/): AsyncEventsDropped counts Lambda async events discarded after age or retry limits. Check throttling, destinations and DLQs, then verify processing recovery.
- [LambdaInsightsExtension: setup and missing metrics fixes](https://polylane.com/learn/troubleshooting/LambdaInsightsExtension/): LambdaInsightsExtension is the AWS Lambda Insights layer name. Choose a verified regional ARN, grant collector log permissions, and diagnose missing metrics.
- [Is It Safe to Give an AI SRE Read Access to Production?](https://polylane.com/learn/ai-in-production/is-it-safe-to-give-an-ai-sre-read-access-to-production/): Read access for an AI SRE is a sound first step if you scope it. The risks are secrets, customer data and exfiltration. Here are the controls and a checklist.
- [Fix 403 Forbidden (Access Denied) from Amazon S3](https://polylane.com/learn/troubleshooting/aws-403-forbidden/): Why Amazon S3 returns 403 Forbidden: Block Public Access, missing s3:ListBucket, object ownership, clock skew, expired presigned URLs and CloudFront. Fix each.
- [Fix 502 Bad Gateway on AWS API Gateway and Load Balancers](https://polylane.com/learn/troubleshooting/aws-502-bad-gateway/): Why AWS returns 502 Bad Gateway from API Gateway or an Application Load Balancer, how to find which hop wrote it, and the fix for each cause.

Get started with one command: `curl -fsSL https://polylane.com/setup | bash` installs the CLI, connects your coding agents, and creates the account.
