A queue consumer often needs time to perform work before it can safely acknowledge a message. Removing the message at receive time would make a consumer crash capable of losing work. Keeping it immediately available would let several consumers process the same item at once.
A visibility timeout occupies the middle ground. Receiving a message makes it temporarily unavailable to competing consumers. The consumer gets a bounded interval to finish and acknowledge it. If that interval expires first, the queue can expose the message for another delivery.
The resulting contract is better modeled as a lease than as ownership. Delivery grants temporary processing rights; acknowledgement completes the handoff.
Receive changes eligibility, not durable existence
Consider a queue containing one ready message:
t0 message is ready
t1 consumer A receives it
t2 message becomes invisible until t1 + 30s
t3 consumer A acknowledges it
t4 message is removed from normal deliveryBetween t1 and t3, the message still matters to the queue even though another consumer cannot normally receive it. Visibility state and durable message state are separate concepts.
If consumer A exits at t2 without acknowledging, the queue does not need a recovery message from that process. Once the lease expires, the original message becomes eligible again.
t0 ready
t1 delivered to A; invisible for 30s
t2 A crashes
t31 lease expires
t32 delivered to BThis recovery path removes a permanent dependency on the health of the consumer that first received the work.
The timeout is part of processing semantics
A visibility timeout that is shorter than normal processing time creates avoidable concurrent deliveries. Suppose a job routinely takes 45 seconds while its visibility interval is 30 seconds. At second 30, another consumer may receive the same message while the first consumer is still working.
A very long timeout has the opposite cost. When a consumer genuinely dies, useful retry work remains hidden until the lease expires.
The interval therefore expresses a trade-off between premature redelivery and recovery latency. A fixed value can be suitable when processing duration is bounded and predictable. Variable workloads often need lease renewal.
receive(message, visibility=30s)
while processing:
if lease_near_expiry:
extend_visibility(message, 30s)
ack(message)Renewal should be tied to real progress or a bounded policy. Extending a lease forever can turn a stuck consumer into a mechanism that suppresses recovery indefinitely.
Acknowledgement must identify the delivery attempt
A message can be delivered more than once across its lifetime. Queue APIs commonly return a receipt, delivery token, or similar handle for the current receive operation. The acknowledgement should use that attempt-specific value rather than treating the stable message ID as sufficient authority.
That distinction matters after a lease expires. Consumer A may retain stale local state while consumer B has already received a newer attempt. An acknowledgement from A must not accidentally complete B’s active delivery.
message id: job-42
attempt A:
receipt: r1
lease: expired
attempt B:
receipt: r2
lease: activeAttempt-scoped receipts let the queue reject or ignore stale completion according to its API contract. The stable message ID remains useful for application-level deduplication, tracing, and business identity, but it serves a different role.
Redelivery makes idempotency an application concern
Visibility leases reduce message loss after consumer failure, but they do not make side effects exactly once.
A consumer can complete an external side effect and fail before acknowledgement reaches the queue:
1. debit operation commits
2. consumer sends acknowledgement
3. connection fails before acknowledgement is accepted
4. visibility lease expires
5. message is delivered againThe second delivery cannot infer from queue state that the debit already committed. The application needs an idempotency boundary around effects that must not repeat.
A common design stores a stable operation key with the business mutation and rejects a second commit carrying the same key. Another design records processed message IDs in the same transaction as the local state change. The correct boundary depends on the resource being changed; a queue acknowledgement cannot make an unrelated database or remote API atomic.
Renewal needs a failure policy
Lease extension is useful for long jobs, but renewal itself can fail. A network partition, overloaded queue service, expired credential, or process stall can prevent an extension from arriving before the current deadline.
Consumers should treat lease expiry as a loss of exclusive processing rights. Continuing an irreversible side effect after the consumer can no longer establish a valid lease may race with a replacement consumer.
Some workloads can stop safely at lease loss. Others need an application-level fencing mechanism, version check, or conditional write at the resource being modified. A queue lease alone cannot fence writes to an external system that does not participate in the lease protocol.
Renewal frequency also deserves a margin. Requesting an extension at the exact expiry boundary leaves no room for network and scheduling delay. Implementations typically renew earlier and retain enough slack for transient latency without turning renewal traffic into a tight loop.
Retry counters and dead-letter policy sit above visibility
Repeated lease expiry can indicate a poison message, a deterministic application error, or a worker that cannot finish within its allocation. Immediate unlimited redelivery can consume capacity without making progress.
Queues often expose a delivery count or support a redrive policy that moves repeatedly failing messages to a dead-letter queue after a configured threshold. That policy is distinct from the visibility timer:
receive
|
+-- success -> acknowledge
|
`-- no acknowledgement
|
v
visibility expires
|
v
delivery count + 1
|
+---+---+
| |
retry dead-letterThe threshold should reflect operational intent. A transient dependency outage may justify retries, while malformed input may fail identically on every attempt. Backoff can keep repeated failures from immediately reclaiming consumer capacity.
Observability should expose lease pressure
Queue depth alone does not show whether consumers are close to losing their leases. Useful signals include the age of the oldest ready message, number of in-flight messages, processing duration, lease-extension count, redelivery count, acknowledgement failures, and dead-letter volume.
A rising extension count can indicate that the initial timeout no longer matches processing duration. Rising redelivery with successful business effects can point to acknowledgement failures or missing idempotency. A growing ready-message age indicates that available consumer throughput is falling behind arrivals.
Per-message tracing is also easier when logs distinguish message identity from delivery-attempt identity. A trace can then show that job-42 was processed under r1, expired, and later completed under r2 without treating both attempts as separate business jobs.
The lease boundary keeps failure recoverable
A visibility timeout does not promise single delivery. It creates a bounded period in which one delivery attempt is hidden from competitors, followed by a deterministic path back to eligibility when completion is absent.
That boundary gives queue systems a practical failure model. Consumers can disappear without permanently owning work, slow jobs can renew their lease, stale attempts can be separated from current attempts, and duplicate effects can be controlled at the application boundary where those effects actually occur.