SQS
SQS is a custom Python reimplementation, not a thin wrapper. All 23 SQS operations are implemented in LocalEmu's own code: standard and FIFO queues, real visibility-timeout tracking, real long polling, dead-letter queues with redrive, message move tasks, CloudWatch metric emission, queue policies, server-side encryption metadata, tags, and persistence. Receiving a message blocks the request thread until a message arrives or the wait timer expires, just like real AWS.
Operation-level coverage: see the SQS coverage matrix.
Quick start
$ awsemu sqs create-queue --queue-name jobs
{
"QueueUrl": "http://sqs.us-east-1.localhost:4566/000000000000/jobs"
}
$ awsemu sqs send-message \
--queue-url http://sqs.us-east-1.localhost:4566/000000000000/jobs \
--message-body '{"task":"resize","file":"img.png"}'
$ awsemu sqs receive-message \
--queue-url http://sqs.us-east-1.localhost:4566/000000000000/jobs \
--max-number-of-messages 1 \
--wait-time-seconds 5
{
"Messages": [{
"MessageId": "a1b2c3...",
"ReceiptHandle": "AQEB...",
"Body": "{\"task\":\"resize\",\"file\":\"img.png\"}",
"Attributes": {
"ApproximateReceiveCount": "1",
"ApproximateFirstReceiveTimestamp": "1747700000000",
"SentTimestamp": "1747699999500",
"SenderId": "AKIAIOSFODNN7EXAMPLE"
}
}]
}
$ awsemu sqs delete-message \
--queue-url http://sqs.us-east-1.localhost:4566/000000000000/jobs \
--receipt-handle "AQEB..." The queue URL returned by create-queue is the standard-format URL (sqs.us-east-1.localhost:4566/<account>/<queue>). Override the URL shape with SQS_ENDPOINT_STRATEGY if you need the domain, path, or dynamic format.
Architecture
SQS lives at services/sqs/. The provider class SqsProvider (provider.py:652) owns four background threads:
- •QueueUpdateWorker at
provider.py:395-461: scans every in-flight message across all queues. Whenvisibility_deadlineelapses without aDeleteMessageorChangeMessageVisibility, the message goes back on the queue. This worker also enforcesMessageRetentionPeriodwhenSQS_ENABLE_MESSAGE_RETENTION_PERIOD=true. - •Delayed-message scheduler at
models.py:330: messages withDelaySeconds > 0sit in a per-queuedelayedset until their delay expires, then graduate to the visible queue. - •CloudWatch metrics emitter at
provider.py:224-361: every 60 seconds (configurable viaSQS_CLOUDWATCH_METRICS_REPORT_INTERVAL), publishes seven metrics per queue to CloudWatch. - •Message-move task executor at
provider.py:1430-1502: drivesStartMessageMoveTaskwhen redriving from a DLQ back to the source.
Queue storage differs by type:
- •Standard queues use an
InterruptiblePriorityQueueatmodels.py:801-814with priority equal to the enqueue timestamp. Approximate FIFO ordering, no strict guarantee, matching real AWS standard semantics. - •FIFO queues at
models.py:1011-1032hold one ordered queue perMessageGroupId. Strict ordering within a group; groups can be consumed in parallel by independent receivers.
When PERSISTENCE=1 is set, the SqsStore (queues + in-flight messages + delayed messages + dedup state) is serialised and restored across restarts via the standard StateVisitor hook (provider.py:684-685).
Configuration
| Variable | Default | Purpose |
|---|---|---|
SQS_ENDPOINT_STRATEGY | standard | Shape of the queue URL returned by CreateQueue and GetQueueUrl. Options: standard (sqs.<region>.<host>/<acct>/<q>), domain ([<region>.]queue.<host>/<acct>/<q>), path (<host>/queue/<region>/<acct>/<q>), dynamic, off. |
SQS_ENABLE_MESSAGE_RETENTION_PERIOD | false | When true, the QueueUpdateWorker deletes messages whose age exceeds MessageRetentionPeriod. Off by default: messages stay in the queue forever until they are received and deleted. |
SQS_DELAY_PURGE_RETRY | false | When true, calling PurgeQueue within 60 seconds of a prior purge raises PurgeQueueInProgress as real AWS does. |
SQS_DELAY_RECENTLY_DELETED | false | When true, re-creating a queue within 60 seconds of deleting it raises QueueDeletedRecently as real AWS does. |
SQS_DISABLE_MAX_NUMBER_OF_MESSAGE_LIMIT | false | When true, ReceiveMessage --max-number-of-messages -1 drains every visible message in one call. Useful for test fixtures. |
SQS_DISABLE_CLOUDWATCH_METRICS | false | When true, the CloudWatch metrics worker stops publishing. |
SQS_CLOUDWATCH_METRICS_REPORT_INTERVAL | 60 | Seconds between CloudWatch metric publications. |
Features supported
| Feature | Notes |
|---|---|
| Queue lifecycle | CreateQueue, GetQueueUrl, ListQueues, DeleteQueue, GetQueueAttributes, SetQueueAttributes. |
| Standard and FIFO queues | FIFO queues must end in .fifo and require FifoQueue=true. |
| Send and receive | SendMessage, SendMessageBatch, ReceiveMessage, DeleteMessage, DeleteMessageBatch. |
| Visibility timeout | ChangeMessageVisibility + ChangeMessageVisibilityBatch. Default 30 s, set per-queue or per-receive. The background worker requeues expired in-flight messages. |
| Long polling | WaitTimeSeconds 0-20 on ReceiveMessage (enforced at provider.py:1166) and as the per-queue ReceiveMessageWaitTimeSeconds default. |
| Message attributes | Types String, Number, Binary. Name regex enforced at provider.py:594-640. |
| System attributes | SenderId, SentTimestamp, ApproximateReceiveCount, ApproximateFirstReceiveTimestamp, AWSTraceHeader, DeadLetterQueueSourceArn. |
| Delay seconds | Per-queue DelaySeconds attribute and per-message DelaySeconds override. |
| Dead-letter queues | RedrivePolicy with deadLetterTargetArn and maxReceiveCount (1-1000). Triggered automatically when the receive-count threshold is exceeded on the source queue. |
| Message move tasks | StartMessageMoveTask, CancelMessageMoveTask, ListMessageMoveTasks for DLQ redrive automation back to the original source queue. |
| Tags | TagQueue, UntagQueue, ListQueueTags. |
| Queue policies | AddPermission, RemovePermission. Policies are stored and returned by GetQueueAttributes but not used for access control on incoming requests. |
| SSE-SQS (managed encryption) | SqsManagedSseEnabled attribute, defaults to true. Metadata only; bodies are stored in cleartext at rest. |
| PurgeQueue | Drops every visible and in-flight message. With SQS_DELAY_PURGE_RETRY=true, a 60-second cooldown blocks a second purge. |
ListDeadLetterSourceQueues | Returns the queues whose RedrivePolicy points at a given DLQ. |
| Persistence | All queue state, including in-flight messages with their receipt handles, is restored across LocalEmu restarts when PERSISTENCE=1. |
FIFO queues
FIFO queues add ordering guarantees and exactly-once delivery within a deduplication window:
- •Strict ordering within a
MessageGroupId. Messages tagged with the same group ID are delivered in the order they were sent. Different group IDs are independent and can be processed in parallel. - •Deduplication by explicit
MessageDeduplicationIdor by SHA-256 of the body whenContentBasedDeduplication=true. Re-sending the same dedup ID within 5 minutes is silently accepted by SQS but the duplicate is dropped. - •Deduplication scope via
DeduplicationScope:queue(default, one dedup window per queue) ormessageGroup(one dedup window perMessageGroupId). - •High-throughput FIFO: the
FifoThroughputLimit=perMessageGroupIdattribute is accepted and stored but the AWS per-group throughput cap is not enforced in LocalEmu.
$ awsemu sqs create-queue --queue-name orders.fifo \
--attributes FifoQueue=true,ContentBasedDeduplication=true
# Three messages, same MessageGroupId => strict order preserved on receive
$ for i in 1 2 3; do
awsemu sqs send-message \
--queue-url http://sqs.us-east-1.localhost:4566/000000000000/orders.fifo \
--message-group-id customer-42 \
--message-body "{\"order\": $i}"
done
$ for i in 1 2 3; do
awsemu sqs receive-message \
--queue-url http://sqs.us-east-1.localhost:4566/000000000000/orders.fifo \
--query 'Messages[0].Body' --output text
done
{"order": 1}
{"order": 2}
{"order": 3} Dead-letter queues and redrive
A redrive policy is attached as a queue attribute. Once a message is received and not deleted before its visibility timeout expires more times than maxReceiveCount, the next would-be redelivery sends it to the dead-letter queue instead. The DLQ receives the original message body plus a DeadLetterQueueSourceArn system attribute pointing at the source queue.
# 1. Create the DLQ first; capture its ARN for the redrive policy
$ awsemu sqs create-queue --queue-name jobs-dlq
$ DLQ_ARN=$(awsemu sqs get-queue-attributes \
--queue-url http://sqs.us-east-1.localhost:4566/000000000000/jobs-dlq \
--attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
# 2. Source queue with a redrive policy: 3 failed receives => DLQ
$ awsemu sqs create-queue --queue-name jobs \
--attributes RedrivePolicy="{\"deadLetterTargetArn\":\"$DLQ_ARN\",\"maxReceiveCount\":\"3\"}"
$ Q=http://sqs.us-east-1.localhost:4566/000000000000/jobs
$ awsemu sqs send-message --queue-url $Q --message-body '{"work":"poisoned"}'
# Receive 3 times without deleting -- visibility timeout expires between each
# receive, so the next ReceiveMessage redelivers
$ for i in 1 2 3; do
awsemu sqs receive-message --queue-url $Q --visibility-timeout 1 > /dev/null
sleep 1.5
done
# The 4th receive on the source queue returns nothing -- the message moved to the DLQ
$ awsemu sqs receive-message --queue-url $Q --query 'Messages' --output text
None
$ awsemu sqs receive-message \
--queue-url http://sqs.us-east-1.localhost:4566/000000000000/jobs-dlq \
--query 'Messages[0].Attributes.DeadLetterQueueSourceArn' --output text
arn:aws:sqs:us-east-1:000000000000:jobs StartMessageMoveTask walks every message currently in a DLQ and re-sends each one to the source queue named in its DeadLetterQueueSourceArn. Use it to replay messages after fixing the consumer.
CloudWatch metrics
The metrics worker publishes seven metrics per queue every 60 seconds (configurable). Disable with SQS_DISABLE_CLOUDWATCH_METRICS=true. All metrics use the AWS/SQS namespace with a QueueName dimension.
| Metric | Source |
|---|---|
NumberOfMessagesSent | services/sqs/provider.py:224-274 |
NumberOfMessagesDeleted | services/sqs/provider.py:276-288 |
NumberOfMessagesReceived | services/sqs/provider.py:290-304 |
NumberOfEmptyReceives | services/sqs/provider.py:305-310 |
ApproximateNumberOfMessagesVisible | services/sqs/provider.py:344-346 |
ApproximateNumberOfMessagesNotVisible | services/sqs/provider.py:348-353 |
ApproximateNumberOfMessagesDelayed | services/sqs/provider.py:355-361 |
Integration points
SQS is a sink for five other LocalEmu services. All five paths are exercised end-to-end:
| Source | How it lands on SQS | Code path |
|---|---|---|
| S3 event notifications | S3 calls SendMessage with the standard Records envelope. | services/s3/notifications.py:436,467 |
| SNS subscriptions | SNS publishes deliver to subscribed queues via SendMessage (batched). | services/sns/publisher.py |
| EventBridge targets | SqsTargetSender dispatches matched events. | services/events/target.py |
| Lambda Event Source Mapping | The SQS poller reads up to BatchSize messages and invokes the function once per batch. | services/lambda_/event_source_mapping/pollers/sqs_poller.py |
| Pipes source | Pipes runs its own SQS poller and forwards each batch through the optional transform to the configured target. | services/pipes/pipe_worker_factory.py |
# Wire S3 ObjectCreated events to an SQS queue
$ awsemu sqs create-queue --queue-name uploads-events
$ QUEUE_ARN=$(awsemu sqs get-queue-attributes \
--queue-url http://sqs.us-east-1.localhost:4566/000000000000/uploads-events \
--attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
$ awsemu s3 mb s3://uploads
$ awsemu s3api put-bucket-notification-configuration --bucket uploads \
--notification-configuration '{
"QueueConfigurations": [{
"QueueArn": "'"$QUEUE_ARN"'",
"Events": ["s3:ObjectCreated:*"]
}]
}'
$ echo "hello" | awsemu s3 cp - s3://uploads/hello.txt
$ awsemu sqs receive-message \
--queue-url http://sqs.us-east-1.localhost:4566/000000000000/uploads-events \
--query 'Messages[0].Body' --output text | python3 -m json.tool
{
"Records": [{
"eventSource": "aws:s3",
"eventName": "ObjectCreated:Put",
"s3": {
"bucket": {"name": "uploads"},
"object": {"key": "hello.txt", "size": 6}
}
}]
} Limits and defaults
| Limit | LocalEmu | AWS SQS | Source |
|---|---|---|---|
| Default visibility timeout | 30 s | 30 s | services/sqs/models.py:371 |
| Default message retention | 4 days | 4 days | services/sqs/models.py:368 |
| Max long-poll wait | 20 s | 20 s | services/sqs/provider.py:1166 |
Max messages per ReceiveMessage | 10 | 10 | services/sqs/provider.py:1184-1188 |
| Queue name max length | 80 | 80 | services/sqs/provider.py:136 |
maxReceiveCount range | 1-1000 | 1-1000 | services/sqs/provider.py:1355 |
| FIFO dedup window | 5 min | 5 min | services/sqs/constants.py:14 |
| Recently-deleted cooldown | 60 s (opt-in) | 60 s | services/sqs/constants.py:18 |
Default MaximumMessageSize | 1 MiB | 256 KiB | services/sqs/constants.py:21 |
LocalEmu's default MaximumMessageSize is 1 MiB, four times the 256 KiB AWS default. A queue created with no explicit MaximumMessageSize accepts messages up to 1 MiB. To match real AWS, pass --attributes MaximumMessageSize=262144 when calling CreateQueue. The other AWS-spec limits not listed above (max retention 14 days, max delay 15 min, max visibility 12 h, max in-flight 120 000 standard / 20 000 FIFO, max 10 message attributes per message) are not enforced by LocalEmu; the values are stored and reported as set but no upper bound is checked.
Known limitations
- •Default
MaximumMessageSizeis 1 MiB, not 256 KiB. See the previous section. - •Queue policies are stored but not enforced.
AddPermission/RemovePermissionupdate thePolicyattribute and the policy is returned byGetQueueAttributes, but cross-account requests are not actually denied based on the policy. - •
SqsManagedSseEnabledis metadata. Bodies are stored in cleartext at rest. SSE-SQS and SSE-KMS attributes are accepted, returned correctly, but no encryption is applied to the stored payload. - •High-throughput FIFO not enforced. The
FifoThroughputLimitattribute is accepted but the per-group rate cap is not applied. - •DLQ chains are not supported. A DLQ cannot itself have a
RedrivePolicypointing at another DLQ. Real AWS supports up to 10 chained DLQs; LocalEmu rejects the second level. - •Message move tasks do not survive a LocalEmu restart. An in-progress
StartMessageMoveTaskstops when the gateway stops; remaining DLQ messages stay in the DLQ until the task is restarted. Tracking:services/sqs/provider.py:473. - •
ListQueuespagination is partial.MaxResultsis honoured butNextTokenis not threaded through correctly. Tracking:services/sqs/provider.py:656-659. - •Per-message KMS keys are not implemented. The queue-level KMS attribute is stored; per-message overrides have no effect.
- •
AWS::SQS::QueueInlinePolicyCloudFormation resource is a stub. UseAWS::SQS::QueuePolicyinstead.