# How to make queue consumers safe for duplicate delivery

> Queue consumer safety depends on idempotent side effects, a correct acknowledgment point, bounded retries, and dead-letter handling. Learn the failure-injection tests to require on agent-written consumers and how Factory Missions validate them.

URL: https://factory.com/articles/queue-consumer-safety-coding-agents
Published: 2026-09-27
Updated: 2026-10-23

A queue consumer is safe when one logical message produces one durable effect no matter how many times it is delivered. Make the side effect idempotent on a stable message ID, acknowledge only after the durable write, bound retries with a dead-letter path, and test duplicate delivery, crashes before acknowledgment, and lease expiry.

Queue consumer safety depends on behavior under duplicate delivery, worker failure, and delayed acknowledgment. Agent-generated code can look correct in a unit test while still charging twice, sending duplicate mail, or losing work when the process exits between a side effect and an acknowledgment.

The delivery contract must come from the queue service. Amazon SQS documents [at-least-once delivery](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/standard-queues-at-least-once-delivery.html) for standard queues and tells consumers to be idempotent. That guarantee should shape the task specification and tests before a coding agent edits the handler.

## Define queue consumer safety invariants

Write down the durable effect for one logical message. A payment event may create one ledger entry. An indexing event may move a record to a target version. A notification may record one send against a stable event identifier. The invariant must survive retries across processes, not only duplicate calls inside one test.

Choose the idempotency boundary beside the side effect. A database-backed consumer can claim a message identifier and apply the state change in one transaction. A remote API may provide its own idempotency key. An in-memory set is useful in a narrow test, but it does not protect multiple workers or restarts.

For a database-backed consumer, the order of operations looks like this. The claim and the side effect share one transaction, and the acknowledgment comes last.

Receive message</span><br/><span style='color:#8A8380 !important'>stable message ID</span>"]
    B["<span style='color:#FAFAFA !important;font-weight:600'>Begin transaction</span>"]
    C{"<span style='color:#FAFAFA !important;font-weight:600'>Message ID already claimed?</span>"}
    D["<span style='color:#FAFAFA !important;font-weight:600'>Skip the side effect</span>"]
    E["<span style='color:#FAFAFA !important;font-weight:600'>Claim ID and apply the effect</span><br/><span style='color:#8A8380 !important'>same transaction</span>"]
    F["<span style='color:#FAFAFA !important;font-weight:600'>Commit, then acknowledge</span>"]
    A --> B --> C
    C -- yes --> D --> F
    C -- no --> E --> F
`}
/>

Keep the message identifier stable across retries. Generating a new key inside the handler defeats deduplication. Define what happens when the same identifier arrives with different content, since silently accepting conflicting payloads can hide producer defects.

## Test queue consumer safety under interruption

Exercise the order of operations directly. Deliver the same message twice, fail after the durable write but before acknowledgment, and restart the worker. The final state should match one successful logical operation.

Test retryable and terminal failures separately. A transient dependency error may return the message to the queue. Invalid input usually belongs in a dead-letter path with enough context for diagnosis. Bound retry counts and backoff according to the service configuration so a poison message cannot consume a worker indefinitely.

Visibility timeouts and leases need tests that outlive the normal handler duration. If work can exceed the lease, verify renewal and cancellation behavior. If the worker loses ownership, prevent it from committing a stale result after another worker has taken over.

Batch consumers need a partial-failure case. Prove which messages are acknowledged when one item fails and whether successful items can reappear. The handler must preserve each message's idempotency boundary even when the queue API reports results for the batch as a group.

Together, these cases make a failure-injection suite. Each row should be a test that runs in CI.

| Scenario | How to inject it | Expected final state |
| --- | --- | --- |
| Duplicate delivery | Deliver the same message twice | One durable effect |
| Crash after write, before acknowledgment | Stop the worker after commit | Redelivery changes nothing |
| Transient dependency error | Fail the downstream call once | Message retried with backoff |
| Invalid payload | Send a malformed message | Dead-letter path with diagnostic context |
| Lease expires mid-work | Hold the handler past the visibility timeout | Stale worker cannot commit |
| Partial batch failure | Fail one item in a batch | Only the failed item reappears |

## Review the complete delivery path

A pull request should include the queue's documented delivery semantics, acknowledgment point, idempotency store, retry policy, dead-letter behavior, and failure-injection results. Review producer and consumer changes together when message identifiers or schemas move.

Keep production replay and queue purging outside the coding task unless separately authorized. Safe code generation produces reviewable changes and isolated evidence. Operators still control actions that affect live messages.

## How Factory validates consumer changes

Factory is a platform for running a software factory, where agents called Droids take work from a signal to a reviewed pull request. Queue work often spans handler code, infrastructure configuration, and restart tests, and Factory has features for each part.

**Plan validation before implementation.** [Missions](https://docs.factory.com/missions/planning) let a team agree on features and milestones before any code is written. Validation workers run at the end of each milestone, so handler logic, queue configuration, and the failure-injection suite can each be a checkpoint.

**Exercise the flow end to end.** Factory's [Automated QA](https://docs.factory.com/software-factory/automated-qa) generates a project QA skill that runs the affected API or CLI flows on each pull request and keeps the evidence in a structured report.

**Review the handler.** Factory's [automated code review](https://docs.factory.com/software-factory/code-review-ci) looks for missing error handling and race conditions, two common sources of lost or duplicated work in consumers. A `review-guidelines` skill can require a stated acknowledgment point and idempotency key for every new handler.

Keep production replay and queue purging outside the coding task unless separately authorized. Operators still control actions that affect live messages.

## Frequently asked questions

### What makes a queue consumer idempotent?

The consumer records a stable message identifier next to the side effect, so a repeated delivery is recognized and skipped. A database-backed consumer can claim the identifier and apply the change in one transaction.

### When should a consumer acknowledge a message?

After the durable effect is committed. Acknowledging earlier can lose work if the process exits, and acknowledging later without idempotency can repeat the effect.

### Why do queues deliver duplicates?

Many queues guarantee at-least-once delivery. Amazon SQS standard queues, for example, can deliver a message more than once and tell consumers to be idempotent.

### What should happen to a message that keeps failing?

Bound the retry count and backoff according to the service configuration, then move the message to a dead-letter path with enough context for diagnosis, so a poison message cannot occupy a worker forever.

### How does Factory help test queue consumer changes?

Factory Missions place validation at milestones, Automated QA runs API or CLI flows around the consumer, and automated code review checks the handler for missing error handling and race conditions.

## Further reading

- [Retry safety](https://factory.com/articles/retry-safety-agent-generated-code)
- [Concurrency testing](https://factory.com/articles/concurrency-testing-coding-agent-changes)
- [Coding agent observability](https://factory.com/articles/coding-agent-observability-changes)
- [Test impact analysis](https://factory.com/articles/test-impact-analysis-coding-agents)
- [Amazon SQS visibility timeout](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html)
