← Back to DevBytes

SQS Best Practices: Cost, Security, and Performance

Introduction to Amazon SQS Best Practices

Amazon Simple Queue Service (SQS) is a fully managed message queuing service that enables you to decouple and scale microservices, distributed systems, and serverless applications. While SQS is remarkably easy to get started with, running it in production at scale requires careful attention to three pillars: cost optimization, security posture, and performance tuning. This tutorial walks through each of these areas with practical examples you can apply immediately.

What Is Amazon SQS?

SQS is a distributed message queuing service that allows producers to send messages to a queue and consumers to retrieve them asynchronously. It offers two queue types: Standard queues, which provide at-least-once delivery and nearly unlimited throughput, and FIFO queues, which guarantee exactly-once processing and message ordering within a message group, but with lower throughput limits.

Unlike broker-based systems such as RabbitMQ or Kafka, SQS is a pull-based service — consumers must poll the queue to retrieve messages. This architectural detail has significant implications for both cost and performance, as we will see throughout this tutorial.

Why Best Practices Matter

Without deliberate design, SQS costs can balloon unexpectedly, security gaps can expose sensitive payloads, and throughput can collapse under load. A common scenario: a team deploys a Lambda consumer polling a Standard queue with short polling and small batch sizes, only to discover their monthly SQS bill is dominated by millions of API requests rather than actual data transfer. Another frequent issue is storing plaintext sensitive data in message bodies, violating compliance requirements. Following best practices from day one prevents these pitfalls.

Getting Started: Creating a Queue

Let's begin by creating a queue using the AWS CLI, then examine how to interact with it programmatically using the AWS SDK for Python (boto3).

# Create a Standard queue with a 4-day retention period
aws sqs create-queue \
  --queue-name orders-standard \
  --attributes VisibilityTimeout=60,MessageRetentionPeriod=345600

# Create a FIFO queue
aws sqs create-queue \
  --queue-name orders.fifo \
  --attributes FifoQueue=true,ContentBasedDeduplication=true,VisibilityTimeout=60

Notice the .fifo suffix — it is mandatory for FIFO queues. The VisibilityTimeout determines how long a message becomes invisible to other consumers after being retrieved, giving the original consumer time to process and delete it.

Sending and Receiving Messages

import boto3

sqs = boto3.client('sqs', region_name='us-east-1')
queue_url = 'https://sqs.us-east-1.amazonaws.com/123456789012/orders-standard'

# Send a message
response = sqs.send_message(
    QueueUrl=queue_url,
    MessageBody='{"order_id": "12345", "amount": 99.99}',
    DelaySeconds=0,
    MessageAttributes={
        'order_type': {
            'DataType': 'String',
            'StringValue': 'priority'
        }
    }
)

# Receive messages with long polling
messages = sqs.receive_message(
    QueueUrl=queue_url,
    MaxNumberOfMessages=10,
    WaitTimeSeconds=20,
    MessageAttributeNames=['All']
)

for msg in messages.get('Messages', []):
    print(f"Processing: {msg['Body']}")
    sqs.delete_message(QueueUrl=queue_url, ReceiptHandle=msg['ReceiptHandle'])

Cost Optimization Best Practices

SQS pricing is based on API requests, not message size or storage duration (within retention limits). This means the number of ReceiveMessage calls often dominates cost. Optimizing cost is largely about reducing unnecessary API calls.

1. Always Use Long Polling

Short polling returns immediately if no messages are available, encouraging consumers to retry rapidly and generate empty receive requests. Long polling, enabled by setting WaitTimeSeconds to a value between 1 and 20, keeps the connection open until a message arrives or the timeout expires. This single change can reduce receive request costs by up to 90%.

# BAD: Short polling - generates many empty requests
response = sqs.receive_message(QueueUrl=queue_url, MaxNumberOfMessages=10)

# GOOD: Long polling - waits up to 20 seconds for messages
response = sqs.receive_message(
    QueueUrl=queue_url,
    MaxNumberOfMessages=10,
    WaitTimeSeconds=20
)

You can also set long polling as a queue attribute using ReceiveMessageWaitTimeSeconds, which applies when the consumer does not specify WaitTimeSeconds explicitly.

2. Batch Your Operations

SQS supports batch operations for sending, receiving, and deleting messages. A single SendMessageBatch call can include up to 10 messages, billed as a single request rather than ten. For high-throughput producers, batching is essential.

entries = [
    {'Id': f'msg-{i}', 'MessageBody': f'{{"order_id": "{i}"}}'}
    for i in range(10)
]

sqs.send_message_batch(QueueUrl=queue_url, Entries=entries)

3. Right-Size Visibility Timeouts

If your visibility timeout is too short, messages become visible again before processing completes, causing duplicate processing and additional API calls. If it is too long, failed consumers delay message redelivery. Set the timeout to slightly longer than your maximum processing time.

4. Use FIFO Queues Only When Necessary

FIFO queues cost more per request than Standard queues and have lower throughput (300 transactions per second without batching, 3000 with batching). Use them only when ordering or exactly-once semantics are truly required.

Security Best Practices

Securing SQS involves protecting the queue itself, the messages in transit, and the data at rest within message bodies.

1. Use IAM Policies for Least Privilege Access

Attach resource-based policies to your queues that restrict access to specific principals and actions. Avoid wildcard permissions in production.

{
  "Version": "2012-10-17",
  "Id": "SQSQueuePolicy",
  "Statement": [
    {
      "Sid": "AllowProducerToSend",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/order-producer"
      },
      "Action": "sqs:SendMessage",
      "Resource": "arn:aws:sqs:us-east-1:123456789012:orders-standard"
    },
    {
      "Sid": "AllowConsumerToReceive",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/order-consumer"
      },
      "Action": [
        "sqs:ReceiveMessage",
        "sqs:DeleteMessage",
        "sqs:GetQueueAttributes"
      ],
      "Resource": "arn:aws:sqs:us-east-1:123456789012:orders-standard"
    }
  ]
}

2. Enable Server-Side Encryption (SSE)

SSE encrypts message bodies at rest using AWS KMS. Enable it on every queue that may contain sensitive data, even if you also encrypt payloads at the application layer.

aws sqs set-queue-attributes \
  --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/orders-standard \
  --attributes KmsMasterKeyId=alias/aws/sqs,SqsManagedSseEnabled=false

For simpler setups, you can use SQS-managed SSE (SSE-SQS) which does not require KMS key management:

aws sqs set-queue-attributes \
  --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/orders-standard \
  --attributes SqsManagedSseEnabled=true

3. Do Not Store Sensitive Data in Message Bodies

SQS is not designed as a secure data store. Instead of placing PII or credentials directly in the message body, store the sensitive data in a secure location (such as an encrypted S3 object or a database row) and include only a reference in the SQS message.

# BAD: Storing sensitive data directly
sqs.send_message(
    QueueUrl=queue_url,
    MessageBody='{"ssn": "123-45-6789", "credit_card": "4111111111111111"}'
)

# GOOD: Store a reference to encrypted data
s3.put_object(Bucket='secure-bucket', Key=f'orders/{order_id}.json', Body=encrypted_payload)
sqs.send_message(
    QueueUrl=queue_url,
    MessageBody=f'{{"order_ref": "s3://secure-bucket/orders/{order_id}.json"}}'
)

4. Restrict Cross-Account Access

If your queue must be accessed from another AWS account, use a condition key to restrict the source account and enforce TLS in transit.

{
  "Effect": "Allow",
  "Principal": { "AWS": "arn:aws:iam::999999999999:root" },
  "Action": "sqs:SendMessage",
  "Resource": "arn:aws:sqs:us-east-1:123456789012:orders-standard",
  "Condition": {
    "Bool": { "aws:SecureTransport": "true" }
  }
}

Performance Best Practices

Performance in SQS is measured by throughput, latency, and consumer efficiency. The following practices help you maximize all three.

1. Use Connection Reuse and SDK Defaults

The AWS SDKs maintain connection pools and retry logic automatically. Avoid creating a new SQS client for every request, and let the SDK handle exponential backoff on throttling errors.

# Create the client once and reuse it
import boto3
from botocore.config import Config

sqs = boto3.client(
    'sqs',
    config=Config(
        max_pool_connections=50,
        retries={'max_attempts': 10, 'mode': 'adaptive'}
    )
)

2. Scale Consumers Horizontally

Because SQS is pull-based, throughput scales with the number of concurrent consumers. For Lambda consumers, set the reserved concurrency appropriately and use the MaxNumberOfMessages parameter to pull batches of up to 10 messages per invocation. For EC2 or container-based consumers, run multiple worker processes per instance.

3. Set Visibility Timeout Based on Processing Time

A good rule of thumb is to set the visibility timeout to 6 times the maximum processing time, plus the value of MaximumBatchingWindowInSeconds if using Lambda. This gives consumers ample time to complete and accounts for retries.

4. Use Dead-Letter Queues

A dead-letter queue (DLQ) captures messages that fail processing after a configured number of attempts. This prevents poison-pill messages from blocking the main queue and allows you to inspect and reprocess failures separately.

aws sqs create-queue --queue-name orders-dlq

aws sqs set-queue-attributes \
  --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/orders-standard \
  --attributes RedrivePolicy='{"deadLetterTargetArn":"arn:aws:sqs:us-east-1:123456789012:orders-dlq","maxReceiveCount":"5"}'

5. Implement Idempotent Consumers

SQS provides at-least-once delivery, meaning consumers may receive the same message more than once. Your processing logic must be idempotent — processing the same message twice should not produce duplicate side effects. Use a deduplication ID or a database unique constraint to guard against duplicates.

def process_message(message):
    order_id = json.loads(message['Body'])['order_id']
    
    # Idempotency check using a database
    if db.exists(f"processed:{order_id}"):
        return  # Already processed, skip
    
    process_order(order_id)
    db.set(f"processed:{order_id}", "1", ex=86400)

6. Leverage Message Attributes and Deduplication

Use message attributes to carry metadata without inflating the message body, which keeps payloads small and processing fast. For FIFO queues, always provide a MessageDeduplicationId if content-based deduplication is disabled.

sqs.send_message(
    QueueUrl=fifo_queue_url,
    MessageBody='{"order_id": "12345"}',
    MessageGroupId='customer-42',
    MessageDeduplicationId='order-12345-v1',
    MessageAttributes={
        'priority': {'DataType': 'Number', 'StringValue': '1'}
    }
)

Monitoring and Observability

Use CloudWatch metrics to monitor queue health. Key metrics include ApproximateNumberOfMessagesVisible, ApproximateAgeOfOldestMessage, and NumberOfMessagesReceived. Set alarms on the oldest message age to detect consumer lag early.

aws cloudwatch put-metric-alarm \
  --alarm-name sqs-old-message-age \
  --metric-name ApproximateAgeOfOldestMessage \
  --namespace AWS/SQS \
  --statistic Maximum \
  --period 300 \
  --threshold 3600 \
  --comparison-operator GreaterThanThreshold \
  --dimensions Name=QueueName,Value=orders-standard \
  --evaluation-periods 1 \
  --alarm-actions arn:aws:sns:us-east-1:123456789012:ops-alerts

Summary Checklist

Conclusion

Amazon SQS is a powerful and deceptively simple service, but getting the most out of it requires intentional design across cost, security, and performance dimensions. By adopting long polling, batching, encryption, least-privilege access, dead-letter queues, and idempotent consumers, you can build resilient, cost-effective, and secure asynchronous workflows that scale gracefully with your application. Start by auditing your existing queues against the checklist above, and incrementally apply these practices as your workloads grow.

— Ad —

Google AdSense will appear here after approval

← Back to all articles