Skip to content

Eliminate the S3 Claim-Check Pattern with SQS 1 MiB Messages

4 minute read
Content level: Intermediate
0

This article shows how the new SQS 1 MiB payload limit lets you send rich event data directly in messages, removing the need for S3 as an intermediary for payloads between 256 KB and 1 MB.

Background

Until August 2025, SQS had a 256 KB message limit. For larger payloads (order events, audit logs, IoT batches), teams used the Claim-Check Pattern: store payload in S3, send the S3 key in SQS, consumer fetches from S3. This adds latency, cost, IAM complexity, and cleanup overhead.

With the 1 MiB payload increase, most of those workarounds are no longer needed.

Prerequisites

  • An SQS queue (Standard or FIFO)
  • No configuration change needed — all queues automatically support 1 MiB

Before: Claim-Check Pattern (256 KB limit)

Producer sends payload to S3, then puts S3 key in SQS:

import boto3, json, uuid

s3 = boto3.client('s3')
sqs = boto3.client('sqs')

def publish_order_event(order_data):
    # Step 1: Upload to S3
    key = f"events/{uuid.uuid4()}.json"
    s3.put_object(Bucket='event-payloads', Key=key, Body=json.dumps(order_data))

    # Step 2: Send reference to SQS
    sqs.send_message(
        QueueUrl=QUEUE_URL,
        MessageBody=json.dumps({"s3_key": key, "bucket": "event-payloads"})
    )

Consumer fetches from S3:

def process(record):
    ref = json.loads(record['body'])
    response = s3.get_object(Bucket=ref['bucket'], Key=ref['s3_key'])
    order = json.loads(response['Body'].read())
    process_order(order)
    s3.delete_object(Bucket=ref['bucket'], Key=ref['s3_key'])  # Cleanup

What this requires: S3 bucket + lifecycle policy + IAM for S3 on both producer and consumer + cleanup logic.

After: Direct 1 MiB Messages

Producer sends the full payload directly:

import boto3, json

sqs = boto3.client('sqs')

def publish_order_event(order_data):
    sqs.send_message(
        QueueUrl=QUEUE_URL,
        MessageBody=json.dumps({"type": "order.created", "payload": order_data})
    )

Consumer processes directly:

def process(record):
    message = json.loads(record['body'])
    process_order(message['payload'])  # Already there — no S3 fetch

What this requires: Just SQS. That's it.

What You Eliminate

ComponentBeforeAfter
S3 BucketRequiredGone
S3 Lifecycle RulesRequiredGone
S3 IAM (producer + consumer)RequiredGone
S3 GET/PUT latency+100-400msGone
Cleanup logicRequiredGone
DLQ debuggingHard (payload in S3)Easy (payload in message)

Cost Comparison (1M messages/month at 500 KB average)

Claim-CheckDirect 1 MiB
SQS$0.40$3.20 (8 chunks × $0.40/M)
S3 PUT + GET$5.40$0.00
Total$5.80$3.20

45% cheaper and architecturally simpler.

When to Still Use Claim-Check

Keep S3 for these scenarios:

  • Payload > 1 MiB — still exceeds the new limit
  • SNS fan-out — payload is copied per subscriber (expensive at 1 MiB × N)
  • Long-term retention — SQS messages are deleted after processing
  • Multiple consumers need same payload — S3 is read-many

Migration Tip

Handle both formats during transition:

def process(record):
    message = json.loads(record['body'])

    if 's3_key' in message:  # Old claim-check format
        payload = fetch_from_s3(message['s3_key'])
    else:  # New direct format
        payload = message['payload']

    process_payload(payload)

Lambda Memory Note

Larger messages use more memory. Adjust your Lambda consumer:

Message SizeRecommended Memory
< 256 KB128-256 MB
256 KB - 1 MiB512 MB - 1 GB

Conclusion

If your messages are between 256 KB and 1 MiB, you can now remove S3 from the message path entirely reducing latency, cost, IAM complexity, and operational overhead. The claim-check pattern isn't dead, but for the majority of event-driven systems, the simplest answer is now: just put it in the message.

Additional Resources