← Back to DevBytes

Migrating from SQS to Pub/Sub: Step-by-Step Guide

What is SQS to Pub/Sub Migration?

Migrating from AWS Simple Queue Service (SQS) to Google Cloud Pub/Sub involves transitioning your messaging infrastructure from a managed message queue system to a fully-managed, scalable pub/sub messaging platform. SQS operates on a pull-based queue model where consumers poll for messages, while Pub/Sub uses a push-or-pull subscription model that decouples publishers from subscribers more completely. This migration is not merely a lift-and-shift operation — it requires rethinking message delivery patterns, acknowledging the architectural differences, and adapting your application code accordingly.

At its core, Pub/Sub offers a global, real-time messaging bus where publishers send messages to topics and subscribers receive them via subscriptions. Unlike SQS queues that are tied to a single consumer group, Pub/Sub allows multiple independent subscriptions per topic, enabling fan-out patterns natively without the need for SNS-SQS combinations.

Why Migrate from SQS to Pub/Sub?

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Several compelling reasons drive teams to migrate from SQS to Pub/Sub:

Key Architectural Differences

Before diving into the migration steps, understanding the conceptual mapping between the two services is critical. The table below outlines the core differences that will shape your migration approach.

Message Lifecycle

In SQS, a message is produced to a queue and remains there until a consumer explicitly deletes it after processing. Visibility timeouts prevent other consumers from re-processing the same message. In Pub/Sub, a message published to a topic is forwarded to all attached subscriptions. Each subscription maintains its own acknowledgment state independently — a single published message can be processed by multiple subscriber applications without coordination.

Delivery Models

SQS is exclusively pull-based. Consumers must poll the queue repeatedly, which can introduce latency and wasted compute cycles. Pub/Sub supports both pull-based delivery (similar to SQS) and push-based delivery, where messages are automatically delivered to a configured endpoint via HTTP. Push subscriptions can significantly simplify consumer architecture, especially for serverless deployments.

Ordering and Deduplication

SQS FIFO queues provide exactly-once processing with ordered delivery but are limited to 3,000 messages per second (with batching). Pub/Sub's ordered message feature uses an ordering key to guarantee that messages with the same key are delivered in order within a subscription, with higher throughput ceilings. Deduplication in Pub/Sub is handled via message IDs and can be configured at the topic level.

Step-by-Step Migration Guide

Step 1: Inventory Your SQS Infrastructure

Begin by cataloging every SQS queue in your system. For each queue, document:

Use the AWS CLI to export queue attributes for documentation:

aws sqs get-queue-attributes --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-queue \
    --attribute-names All --output json > sqs-queue-inventory.json

Step 2: Provision Pub/Sub Resources

For each SQS queue you identified, create a corresponding Pub/Sub topic. If an SQS queue receives messages from multiple producer types that should remain logically separated, consider whether separate topics are warranted. Use the Google Cloud Console or gcloud CLI:

gcloud pubsub topics create order-processing-topic \
    --message-retention-duration=7d \
    --topic-encryption-key=projects/my-project/locations/global/keyRings/my-keyring/cryptoKeys/my-key

Next, create subscriptions. Each SQS consumer group maps to a Pub/Sub subscription. For a standard SQS queue with one consumer, create a single pull subscription:

gcloud pubsub subscriptions create order-processing-sub \
    --topic=order-processing-topic \
    --ack-deadline=600s \
    --message-retention-duration=7d \
    --enable-exactly-once-delivery

For scenarios where SNS fans out to multiple SQS queues, simply create multiple subscriptions on the same Pub/Sub topic — each independent subscription receives every message published to the topic.

Step 3: Adapt Producer Code

Producers in SQS use the SendMessage or SendMessageBatch API. In Pub/Sub, the equivalent is publishing to a topic. Below is a Python example showing both the SQS producer (left) and its Pub/Sub equivalent (right).

Original SQS Producer (Python with boto3):

import boto3
import json
import uuid

sqs = boto3.client('sqs', region_name='us-east-1')
QUEUE_URL = 'https://sqs.us-east-1.amazonaws.com/123456789012/order-queue'

def publish_order(order_data):
    response = sqs.send_message(
        QueueUrl=QUEUE_URL,
        MessageBody=json.dumps(order_data),
        MessageAttributes={
            'orderId': {
                'DataType': 'String',
                'StringValue': order_data['orderId']
            },
            'source': {
                'DataType': 'String',
                'StringValue': 'web-app'
            }
        },
        MessageGroupId=order_data['customerId'],  # FIFO only
        MessageDeduplicationId=str(uuid.uuid4())   # FIFO only
    )
    return response['MessageId']

Migrated Pub/Sub Publisher (Python with google-cloud-pubsub):

from google.cloud import pubsub_v1
import json

publisher = pubsub_v1.PublisherClient()
topic_path = publisher.topic_path('my-project', 'order-processing-topic')

def publish_order(order_data):
    # Pub/Sub attributes map to SQS message attributes
    future = publisher.publish(
        topic_path,
        data=json.dumps(order_data).encode('utf-8'),
        orderId=order_data['orderId'],     # attribute key-value
        source='web-app',
        # ordering_key enables ordered delivery per key
        ordering_key=order_data['customerId']
    )
    message_id = future.result()
    return message_id

Key points in the producer migration:

Step 4: Adapt Consumer Code

SQS consumers poll the queue in a loop, receive messages, process them, and then delete them. Pub/Sub pull consumers follow a similar pattern but use a streaming pull for higher throughput and must acknowledge messages rather than delete them.

Original SQS Consumer:

import boto3
import time

sqs = boto3.client('sqs', region_name='us-east-1')
QUEUE_URL = 'https://sqs.us-east-1.amazonaws.com/123456789012/order-queue'

def process_messages():
    while True:
        response = sqs.receive_message(
            QueueUrl=QUEUE_URL,
            MaxNumberOfMessages=10,
            WaitTimeSeconds=20,  # long polling
            VisibilityTimeout=300
        )
        messages = response.get('Messages', [])
        for msg in messages:
            try:
                body = msg['Body']
                handle_order(body)
                sqs.delete_message(
                    QueueUrl=QUEUE_URL,
                    ReceiptHandle=msg['ReceiptHandle']
                )
            except Exception:
                # Message will become visible again after timeout
                print(f"Failed to process {msg['MessageId']}")
        if not messages:
            time.sleep(1)

Migrated Pub/Sub Consumer (Synchronous Pull):

from google.cloud import pubsub_v1

subscriber = pubsub_v1.SubscriberClient()
subscription_path = subscriber.subscription_path(
    'my-project', 'order-processing-sub'
)

def process_messages():
    while True:
        response = subscriber.pull(
            subscription=subscription_path,
            max_messages=10,
            timeout=30.0   # long-polling timeout in seconds
        )
        received = response.received_messages
        if not received:
            continue

        ack_ids = []
        for msg in received:
            try:
                data = msg.message.data.decode('utf-8')
                handle_order(data)
                ack_ids.append(msg.ack_id)
            except Exception:
                # Do not acknowledge — message will be redelivered
                print(f"Failed to process {msg.message.message_id}")

        if ack_ids:
            subscriber.acknowledge(
                subscription=subscription_path,
                ack_ids=ack_ids
            )

Migrated Pub/Sub Consumer (Streaming Pull — Recommended):

from google.cloud import pubsub_v1

subscriber = pubsub_v1.SubscriberClient()
subscription_path = subscriber.subscription_path(
    'my-project', 'order-processing-sub'
)

def handle_message(message):
    try:
        data = message.data.decode('utf-8')
        handle_order(data)
        message.ack()   # acknowledge on success
    except Exception:
        message.nack()  # nack triggers redelivery
        print(f"Failed to process {message.message_id}")

# Streaming pull manages long-lived connection
streaming_pull_future = subscriber.subscribe(
    subscription_path,
    callback=handle_message,
    flow_control=pubsub_v1.types.FlowControl(
        max_messages=10
    )
)

# Block indefinitely or wrap in try/except for graceful shutdown
try:
    streaming_pull_future.result()
except KeyboardInterrupt:
    streaming_pull_future.cancel()

Important consumer migration notes:

Step 5: Migrate Dead Letter Handling

SQS dead letter queues (DLQs) require you to create a separate queue and configure a redrive policy. Pub/Sub integrates dead letter topics natively into subscriptions. You create a dead letter topic and configure the subscription to forward messages that exceed the maximum delivery attempts.

# Create the dead letter topic
gcloud pubsub topics create order-dlq-topic

# Update subscription with dead letter policy
gcloud pubsub subscriptions update order-processing-sub \
    --dead-letter-topic=order-dlq-topic \
    --max-delivery-attempts=5 \
    --min-dead-letter-delivery-retry=600s

To replay dead-lettered messages back to the original topic (equivalent to SQS redrive), use the Pub/Sub replay functionality:

# Create a subscription on the DLQ topic to pull dead-lettered messages
gcloud pubsub subscriptions create dlq-replay-sub --topic=order-dlq-topic

# Use the subscription to pull and re-publish messages to the original topic
# This is a simplified replay script
from google.cloud import pubsub_v1

publisher = pubsub_v1.PublisherClient()
subscriber = pubsub_v1.SubscriberClient()
dlq_sub_path = subscriber.subscription_path('my-project', 'dlq-replay-sub')
original_topic_path = publisher.topic_path('my-project', 'order-processing-topic')

def replay_dead_letters():
    while True:
        response = subscriber.pull(
            subscription=dlq_sub_path,
            max_messages=50,
            timeout=30.0
        )
        ack_ids = []
        for msg in response.received_messages:
            # Re-publish to original topic
            publisher.publish(
                original_topic_path,
                data=msg.message.data,
                **dict(msg.message.attributes)
            )
            ack_ids.append(msg.ack_id)

        if ack_ids:
            subscriber.acknowledge(
                subscription=dlq_sub_path,
                ack_ids=ack_ids
            )
        else:
            break

Step 6: Handle Push Subscriptions for Serverless Consumers

If your SQS consumers run as Lambda functions triggered by SQS events, the closest Pub/Sub equivalent is a push subscription targeting a Cloud Function or Cloud Run service. This eliminates the polling layer entirely — Pub/Sub delivers messages via HTTP POST.

gcloud pubsub subscriptions create order-push-sub \
    --topic=order-processing-topic \
    --push-endpoint=https://us-central1-my-project.cloudfunctions.net/orderHandler \
    --push-auth-service-account=order-handler-sa@my-project.iam.gserviceaccount.com \
    --ack-deadline=600s \
    --max-delivery-attempts=5 \
    --dead-letter-topic=order-dlq-topic

The Cloud Function receives the message in the standard Pub/Sub push format:

def order_handler(event, context):
    # Pub/Sub push delivers base64-encoded data
    import base64, json

    pubsub_message = event['data']
    message_data = base64.b64decode(pubsub_message).decode('utf-8')
    attributes = event.get('attributes', {})

    order = json.loads(message_data)
    process_order(order)
    # No explicit ack needed — HTTP 200 response acknowledges the message
    return 'OK', 200

Step 7: Testing and Cutover Strategy

A dual-write approach minimizes risk during migration. Modify your producers to publish to both SQS and Pub/Sub during a transition period, while consumers gradually switch over. Validate message integrity, ordering, and processing correctness before decommissioning SQS.

# Dual-write pattern during migration
def publish_order_dual(order_data):
    # Write to SQS (existing path)
    sqs.send_message(
        QueueUrl=SQS_QUEUE_URL,
        MessageBody=json.dumps(order_data),
        MessageAttributes={...}
    )
    # Write to Pub/Sub (new path)
    publisher.publish(
        topic_path,
        data=json.dumps(order_data).encode('utf-8'),
        ordering_key=order_data['customerId'],
        **attributes_dict
    )

Run validation checks comparing SQS and Pub/Sub message counts, consumer throughput, and error rates over a window of at least 24 hours before cutting over entirely. Use Cloud Monitoring to track subscription metrics like subscription/ack_message_count and subscription/dead_letter_message_count.

Best Practices for a Smooth Migration

Conclusion

Migrating from SQS to Pub/Sub is a strategic move that unlocks native fan-out, push-based delivery, global topic management, and tighter integration with Google Cloud's serverless ecosystem. The migration process — inventorying queues, provisioning topics and subscriptions, adapting producer and consumer code, and handling dead letter patterns — is methodical but manageable with a dual-write transition strategy. By understanding the conceptual mapping between SQS queues and Pub/Sub topics/subscriptions, and by leveraging streaming pull, exactly-once delivery, and push subscriptions where appropriate, you can complete the migration with minimal disruption while positioning your messaging infrastructure for greater scale and flexibility.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles