Technology

Messaging Systems Compared: Kafka, RabbitMQ, AWS SQS & SNS

A comprehensive guide to modern messaging systems. Learn the core concepts, best use cases, and hands-on implementation patterns for Apache Kafka, RabbitMQ, AWS SQS, and AWS SNS — and discover which tool to reach for in any situation.

30 min read
Published

Kafka Tutorial

Interactive Kafka app built with Next.js and KafkaJS. Create topics, produce messages, consume them, and inspect consumer groups.

View on GitHub

RabbitMQ Tutorial

Interactive demos for all five exchange types built with Next.js and amqplib. Fanout, Direct, Topic, Headers, and Dead Letter Queues.

View on GitHub

SQS & SNS Tutorial

Full messaging demo with Next.js 16 and AWS SDK v3. Covers SQS queuing and SNS pub/sub patterns with real AWS integration.

View on GitHub

Introduction

Modern distributed systems rely on messaging infrastructure to decouple components, handle asynchronous workloads, and scale reliably. But not all messaging systems are created equal — Apache Kafka, RabbitMQ, AWS SQS, and AWS SNS each solve different problems with fundamentally different architectures.

This guide consolidates three hands-on tutorials into one comprehensive reference. You will learn the core concepts behind each system, see real code examples, and walk away with a clear mental model for choosing the right tool for any scenario.

Summary Decision Matrix

Before diving into each system, here is a high-level comparison to orient your thinking:

FeatureApache KafkaRabbitMQAWS SQSAWS SNS
Primary Use CaseMassive event streaming, log aggregation, event replayComplex message routing, traditional task queuesSimple serverless task queues, buffering/load-levelingBroadcasting messages, Fan-out, User notifications
ArchitectureDistributed Log / Append-onlyMessage Broker (Exchanges & Queues)Simple Point-to-Point QueuePub/Sub (Push based)
Message RetentionDays, weeks, or indefinitelyDeleted immediately after consumption (Ack)Up to 14 daysNo retention (fire-and-forget unless queued)
Consumer ModelPull (via Offsets)Push (default) or PullPull (Long-polling)Push (to HTTP, Lambda, SQS, Email, SMS)
Replayability✅ Yes (Native offset resetting)❌ No (Once consumed, it's gone)❌ No❌ No
Routing LogicVery basic (Topics/Partitions)Highly Advanced (Regex, Headers, Routing Keys)None (1 Queue = 1 destination)Basic (Subscription filters)
Overhead/OpsHigh (Requires KRaft, clustering, tuning)Medium (Server management, Erlang VM, clustering)Zero (Fully managed by AWS)Zero (Fully managed by AWS)

Part 1: Apache Kafka — The Event Streamer & Storage

Overview

An interactive web app for learning Apache Kafka fundamentals, built with Next.js and KafkaJS. The app connects to a live Kafka broker and lets you create topics, produce messages, consume them, and inspect consumer groups — all from the browser.

Prerequisites

Step 1 — Start Kafka in Docker

This tutorial uses a single-broker Kafka setup via Docker Compose. The easiest way is to use the conduktor-kafka-single.yml stack, which starts Kafka (accessible on localhost:9092) and an optional web UI.

Option A: Minimal single-broker Kafka (no UI)

Create a file called docker-compose.yml in any directory:

version: "3"
services:
  kafka:
    image: confluentinc/cp-kafka:7.6.0
    container_name: kafka1
    ports:
      - "9092:9092"
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_LOG_DIRS: /var/lib/kafka/data
      CLUSTER_ID: MkU3OEVBNTcwNTJENDM2Qk

Then start it:

docker compose up -d

# Verify Kafka is running
docker ps
# You should see a container named "kafka1"

Option B: Kafka + Conduktor UI (recommended for visual exploration)

# Clone the docker-compose repo
git clone https://github.com/conduktor/kafka-stack-docker-compose.git
cd kafka-stack-docker-compose

# Start single-broker Kafka + Conduktor web UI
docker compose -f conduktor-kafka-single.yml up -d
  • • Kafka broker: localhost:9092
  • • Conduktor UI: http://localhost:8080 (login with the credentials in the yml file)

Step 2 — Install Dependencies & Run the App

  1. 1
    Clone the repository:
    git clone https://github.com/audoir/kafka-tutorial.git cd kafka-tutorial
  2. 2
    Install dependencies:
    npm install
  3. 3
    Start the development server:
    npm run dev

Open http://localhost:3000 in your browser.

Step 3 — Tutorial Walkthrough

The app has five tabs. Work through them in order for the best learning experience.

📚 Tab 1: Concepts

Read through the concept cards before touching anything else. This tab covers all the core Kafka theory you need:

ConceptKey Takeaway
Topics & PartitionsA topic is a named stream split into partitions. Each message gets an immutable offset per partition.
Producers & Message KeysNo key → round-robin across partitions. With a key → same key always lands on the same partition (ordering guarantee).
Consumers & DeserializationPull model. Reads messages in offset order within each partition.
Consumer Groups & OffsetsA group shares partitions among its consumers. Kafka tracks progress in __consumer_offsets.
Brokers & ReplicationA cluster of brokers. Each partition has one leader broker. Replication factor > 1 = fault tolerance.
KRaft (No ZooKeeper)Kafka 3.3.1+ uses the built-in Raft protocol. ZooKeeper is gone in Kafka 4.x. Never put ZooKeeper in your client config.

The bottom of the page shows 8 common Kafka use cases (messaging, activity tracking, log aggregation, stream processing, etc.).

📋 Tab 2: Topics

What to do: Create a topic, then inspect it.

  1. Enter a name like demo-topic, set Partitions to 3, leave Replication Factor at 1, click Create Topic
  2. Click 🔄 Refresh to fetch all topics — you'll see your new topic along with internal topics like __consumer_offsets
  3. Try creating topics with 1 vs. 5 partitions. Delete topics you no longer need with the 🗑 button

💡 More partitions = more parallelism, but you can't reduce partition count after creation.

📤 Tab 3: Producer

What to do: Send messages to your topic and observe partition routing.

  • Experiment 1 — No key (round-robin): Leave Key blank, send several messages, watch the Delivery Receipts — partition number will vary
  • Experiment 2 — With a key (sticky routing): Set Key to user-123, send multiple times — same key always lands on the same partition
  • Experiment 3 — Demo batch (truck GPS): Click Send Demo Batch (truck GPS) — sends 5 messages with keys truck-1, truck-2, truck-3

💡 Message keys are how you guarantee ordering for a specific entity (user, device, order) across multiple messages.

📥 Tab 4: Consumer

What to do: Read messages back from Kafka and observe offset/partition behavior.

  • Experiment 1: Set Group ID to group-a, check From beginning, consume — you get all messages
  • Experiment 2: Consume again with group-a (uncheck "From beginning") — you get 0 messages (offsets already committed)
  • Experiment 3: Change to group-b and consume from beginning — you get all messages again

💡 Kafka doesn't delete messages when consumed. Multiple independent consumer groups can each read the full topic at their own pace.

👥 Tab 5: Consumer Groups

What to do: Understand partition assignment and fetch live groups.

ScenarioWhat happens
3 partitions, 3 consumersPerfect — one partition per consumer
3 partitions, 2 consumersConsumer A gets 2 partitions, Consumer B gets 1
3 partitions, 4 consumersConsumer D is idle — more consumers than partitions
Multiple groups on same topicEach group reads independently (pub/sub pattern)

Also covers Eager vs. Cooperative rebalancing and At-most-once / At-least-once / Exactly-once delivery semantics. Click 🔄 Fetch Groups to see live consumer groups from your experiments.

How the Code Works — KafkaJS Client

lib/kafka.ts is the single source of truth for all Kafka connections in the app:

// lib/kafka.ts — The KafkaJS Client singleton
import { Kafka } from 'kafkajs';

const kafka = new Kafka({
  clientId: 'kafka-tutorial',
  brokers: ['localhost:9092'],
});

// A shared admin client — used for topic management and cluster inspection
export const admin = kafka.admin();

// A shared producer — used to send messages to topics
export const producer = kafka.producer();

// A factory function — creates a new consumer with a given group ID
export function createConsumer(groupId: string) {
  return kafka.consumer({ groupId });
}

Why singletons for admin and producer? KafkaJS clients maintain persistent TCP connections to the broker. Reusing a single instance avoids the overhead of creating a new connection on every API request. Each API route calls .connect() before use and .disconnect() after. Why a factory for consumers? Each consumer must belong to a specific group, and different tutorial experiments use different group IDs.

API Routes

All routes live under app/api/ and follow the Next.js App Router Route Handler convention. Each file exports named HTTP method functions (GET, POST, DELETE). All routes set export const dynamic = "force-dynamic" to opt out of caching.

RouteDescription
GET /api/healthCalls admin.describeCluster() to verify the broker is reachable. Returns cluster ID, controller ID, and broker list. Returns HTTP 503 on failure.
GET /api/topicsLists all topics via admin.listTopics(), then fetches partition metadata (leader, replicas, ISR).
POST /api/topicsCreates a topic. Body: { topic, numPartitions, replicationFactor }
DELETE /api/topicsDeletes a topic. Body: { topic }
POST /api/produceSends one or more messages to a topic. Returns delivery receipts (partition + baseOffset per message).
POST /api/consumeSubscribes a consumer to a topic, collects messages for up to 3 seconds, then disconnects. Returns { partition, offset, key, value, timestamp } objects.
GET /api/consumer-groupsLists all consumer groups via admin.listGroups(). Returns group IDs and protocol types.

Project Structure

kafka-tutorial/
├── app/
│   ├── api/
│   │   ├── health/route.ts          # GET — ping Kafka, return broker/cluster info
│   │   ├── topics/route.ts          # GET list, POST create, DELETE topic
│   │   ├── produce/route.ts         # POST send messages
│   │   ├── consume/route.ts         # POST consume messages
│   │   └── consumer-groups/route.ts # GET list groups
│   ├── layout.tsx
│   └── page.tsx                     # Main tabbed UI + connection status badge
├── components/
│   ├── ConceptsPanel.tsx            # Theory reference with diagrams
│   ├── TopicsPanel.tsx              # Create/list/delete topics
│   ├── ProducerPanel.tsx            # Send messages with/without keys
│   ├── ConsumerPanel.tsx            # Consume and inspect messages
│   └── ConsumerGroupsPanel.tsx      # Group scenarios + live group list
└── lib/
    └── kafka.ts                     # KafkaJS client singleton (broker: localhost:9092)

Stopping Kafka

# If using the minimal setup
docker compose down

# If using conduktor
docker compose -f conduktor-kafka-single.yml down

Part 2: RabbitMQ — The Smart Message Router

Overview

An interactive Next.js application for learning RabbitMQ exchange types. Each exchange type has a dedicated tutorial page with explanations, a code structure overview, and a live demo you can run against a real RabbitMQ instance.

Prerequisites

1. Set Up RabbitMQ with Docker

Option A: Quick start (single command)

docker run -d \
  --name rabbitmq \
  -p 5672:5672 \
  -p 15672:15672 \
  rabbitmq:management

Option B: With persistent data (recommended)

docker run -d \
  --name rabbitmq \
  -p 5672:5672 \
  -p 15672:15672 \
  -v rabbitmq_data:/var/lib/rabbitmq \
  --hostname rabbitmq-host \
  rabbitmq:management

Option C: Using Docker Compose

version: "3.8"
services:
  rabbitmq:
    image: rabbitmq:management
    container_name: rabbitmq
    ports:
      - "5672:5672"   # AMQP protocol port
      - "15672:15672" # Management UI port
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq
    environment:
      RABBITMQ_DEFAULT_USER: guest
      RABBITMQ_DEFAULT_PASS: guest

volumes:
  rabbitmq_data:
docker compose up -d

Open the Management UI at http://localhost:15672 — Username: guest, Password: guest. The AMQP port 5672 is used by the application.

Useful Docker commands

# Check if RabbitMQ is running
docker ps | grep rabbitmq

# View logs
docker logs rabbitmq

# Stop RabbitMQ
docker stop rabbitmq

# Start again
docker start rabbitmq

# Remove container (data lost unless using volume)
docker rm -f rabbitmq

2. Set Up the Next.js App

  1. 1
    Clone the repository:
    git clone https://github.com/audoir/rabbitmq-tutorial.git
  2. 2
    Install dependencies:
    npm install
  3. 3
    Start the development server:
    npm run dev

Open http://localhost:3000 to see the tutorial home page. The home page shows a live connection status indicator — green means RabbitMQ is reachable.

3. Environment Variables (Optional)

By default the app connects to amqp://guest:guest@localhost:5672. To use a different RabbitMQ instance, create a .env.local file:

RABBITMQ_URL=amqp://username:password@your-host:5672

4. How the Code is Structured

Each exchange type follows a strict Setup → Publish → Consume pattern:

RouteResponsibility
setup/route.tsCreates the exchange, queues, and bindings. Must be called first.
publish/route.tsPublishes a message to the exchange. Assumes setup has been done.
consume/route.tsReads messages from a queue. Assumes setup has been done.

Why? In RabbitMQ, messages are routed to queues at the moment of publishing. If a queue doesn't exist yet (or isn't bound to the exchange), the message is silently dropped. The setup step ensures all queues and bindings exist before any messages are sent.

5. Tutorial Walkthrough

Follow the tutorials in order for the best learning experience. On each page, always click Setup first before publishing.

Step 1 — Fanout Exchange (/tutorial/fanout)

A fanout exchange broadcasts every message to all bound queues. The routing key is ignored.

Try it:

  1. Open http://localhost:3000/tutorial/fanout
  2. Click Setup Exchange & Queues
  3. Click Publish to send a message
  4. Consume from fanout.queue.1 — you get the message
  5. Consume from fanout.queue.2 — you get the same message again!

💡 One publish → all queues receive a copy.

// Fanout: one publish → all queues receive a copy
channel.publish('fanout.exchange', '', Buffer.from(message));
// Routing key is empty string — it's ignored by fanout exchanges

Step 2 — Direct Exchange (/tutorial/direct)

A direct exchange routes messages to queues whose binding key exactly matches the routing key.

Try it:

  1. Open http://localhost:3000/tutorial/direct
  2. Click Setup Exchange & Queues
  3. Publish with routing key error
  4. Consume with routing key error → you get the message ✓
  5. Consume with routing key info → you get nothing ✗

💡 Exact match routing — like a postal address.

// Direct: exact routing key match
channel.publish('direct.exchange', 'error', Buffer.from(message));
// Only queues bound with key 'error' receive this message

Step 3 — Topic Exchange (/tutorial/topic)

A topic exchange routes using wildcard pattern matching on dot-separated routing keys.* matches exactly one word; # matches zero or more words.

Try it:

  1. Open http://localhost:3000/tutorial/topic
  2. Click Setup Exchange & Queues
  3. Publish with key logs.error
  4. Publish with key logs.error.critical
  5. Consume with pattern logs.* → gets only logs.error (one word after logs)
  6. Consume with pattern logs.# → gets both messages (any words after logs)

💡 Flexible pattern-based routing.

// Topic: wildcard pattern matching
// Pattern 'logs.*' matches 'logs.error' but NOT 'logs.error.critical'
// Pattern 'logs.#' matches both 'logs.error' and 'logs.error.critical'

channel.bindQueue('queue.star', 'topic.exchange', 'logs.*');
channel.bindQueue('queue.hash', 'topic.exchange', 'logs.#');

Step 4 — Headers Exchange (/tutorial/headers)

A headers exchange routes based on message header attributes. The routing key is ignored. Use x-match: all for AND logic or x-match: any for OR logic.

Try it:

  1. Open http://localhost:3000/tutorial/headers
  2. Click Setup Exchange & Queues
  3. Publish with headers {"format":"pdf","type":"report"}
  4. Consume with x-match=all and headers {"format":"pdf","type":"report"} → match ✓
  5. Consume with x-match=all and headers {"format":"pdf","type":"invoice"} → no match ✗
  6. Consume with x-match=any and headers {"format":"pdf","type":"invoice"} → match ✓ (format matches)

💡 Attribute-based routing without routing keys.

// Headers: attribute-based routing
channel.publish('headers.exchange', '', Buffer.from(message), {
  headers: { format: 'pdf', type: 'report' }
});

// Queue bound with x-match=all, format=pdf, type=report → MATCH ✓
// Queue bound with x-match=all, format=pdf, type=invoice → NO MATCH ✗
// Queue bound with x-match=any, format=pdf, type=invoice → MATCH ✓

Step 5 — Dead Letter Queue (/tutorial/deadletter)

A Dead Letter Queue (DLQ) captures messages that cannot be processed. Messages become "dead letters" when: a consumer rejects them with nack(msg, false, false), they expire (TTL elapsed), or the queue exceeds its length limit.

Try it (rejection):

  1. Open http://localhost:3000/tutorial/deadletter
  2. Click Setup Exchanges & Queues
  3. Click Publish to Main Queue
  4. Click Reject Message (NACK) — simulates a failed consumer
  5. Click Consume from DLQ — see the rejected message with x-death metadata

Try it (TTL expiry):

  1. After setup, publish with TTL = 3000ms
  2. Wait 3 seconds (don't reject)
  3. Click Consume from DLQ — message expired and moved automatically

💡 Never lose a message — capture failures in a DLQ for inspection and retry.

// Setup: main queue with dead letter exchange configured
channel.assertQueue('main.queue', {
  durable: true,
  arguments: {
    'x-dead-letter-exchange': 'dlx.exchange',
    'x-message-ttl': 5000  // optional: auto-expire after 5s
  }
});

// Reject a message → routes to DLX → lands in dead letter queue
channel.nack(msg, false, false);  // requeue=false → dead letter

// x-death header added by RabbitMQ:
// { queue, reason: 'rejected'|'expired'|'maxlen', time, exchange, routing-keys }

6. Project Structure

app/
├── page.tsx                    # Tutorial home page
├── api/
│   ├── status/route.ts         # GET: check RabbitMQ connection
│   ├── fanout/
│   │   ├── setup/route.ts      # POST: create exchange, queues, bindings
│   │   ├── publish/route.ts    # POST: publish to fanout exchange
│   │   └── consume/route.ts    # GET: consume from a named queue
│   ├── direct/
│   │   ├── setup/route.ts      # POST: create exchange, queues, bindings
│   │   ├── publish/route.ts    # POST: publish with routing key
│   │   └── consume/route.ts    # GET: consume by routing key
│   ├── topic/
│   │   ├── setup/route.ts      # POST: create exchange, queues, bindings
│   │   ├── publish/route.ts    # POST: publish with dot-separated key
│   │   └── consume/route.ts    # GET: consume with wildcard pattern
│   ├── headers/
│   │   ├── setup/route.ts      # POST: create exchange, queues, bindings
│   │   ├── publish/route.ts    # POST: publish with custom headers
│   │   └── consume/route.ts    # GET: consume with x-match filter
│   └── deadletter/
│       ├── setup/route.ts      # POST: create both exchanges, both queues, bindings
│       ├── publish/route.ts    # POST: publish to main queue (optional TTL)
│       ├── reject/route.ts     # POST: NACK a message → routes to DLX
│       └── consume/route.ts    # GET: consume from dead letter queue
└── tutorial/
    ├── fanout/page.tsx         # Fanout tutorial page
    ├── direct/page.tsx         # Direct tutorial page
    ├── topic/page.tsx          # Topic tutorial page
    ├── headers/page.tsx        # Headers tutorial page
    └── deadletter/page.tsx     # Dead letter queue tutorial page

lib/
└── rabbitmq.ts                 # Shared: createConnection / closeConnection

7. Exchange Type Quick Reference

TypeRouting LogicRouting Key?Best For
FanoutAll bound queues❌ IgnoredBroadcasting, notifications
DirectExact key match✅ Exact matchLog levels, task routing
TopicPattern matching✅ Wildcards (* #)Flexible routing, microservices
HeadersHeader attributes❌ IgnoredAttribute-based routing

8. RabbitMQ Concepts

Publisher → Exchange → Queue → Consumer
PublisherSends messages to an exchange (never directly to a queue)
ExchangeRoutes messages to queues based on type and binding rules
QueueStores messages until a consumer retrieves them
ConsumerReceives messages; load is balanced across multiple consumers
Supported Protocols:AMQP (used by this tutorial, port 5672), STOMP, MQTT, RabbitMQ Streams
Queue Configuration:
  • durable — survives broker restart
  • auto-delete — deleted when last consumer disconnects
  • exclusive — only accessible by the declaring connection
Dead Letter Headers (x-death):
  • queue — original queue name
  • reasonrejected, expired, or maxlen
  • time — when it was dead-lettered
  • exchange — original exchange
  • routing-keys — original routing keys

Shared Connection Helper

// lib/rabbitmq.ts
import amqp from 'amqplib';

const RABBITMQ_URL = process.env.RABBITMQ_URL || 'amqp://guest:guest@localhost:5672';

export async function createConnection() {
  const connection = await amqp.connect(RABBITMQ_URL);
  const channel = await connection.createChannel();
  return { connection, channel };
}

export async function closeConnection(
  channel: amqp.Channel,
  connection: amqp.Connection
) {
  await channel.close();
  await connection.close();
}

9. Troubleshooting

"RabbitMQ Not Connected" on the home page
  • • Make sure Docker is running: docker ps | grep rabbitmq
  • • Start RabbitMQ: docker start rabbitmq
  • • Check logs: docker logs rabbitmq
Port already in use
# Check what's using port 5672
lsof -i :5672
# Or use different ports
docker run -d --name rabbitmq -p 5673:5672 -p 15673:15672 rabbitmq:management
# Then set RABBITMQ_URL=amqp://guest:guest@localhost:5673 in .env.local
Queue already exists with different parameters
  • • Go to the Management UI → Queues → delete the conflicting queue
  • • Or restart RabbitMQ: docker restart rabbitmq
Messages not appearing after publish

Make sure you clicked Setup before publishing. If the queues didn't exist at publish time, the messages were silently dropped by RabbitMQ. Click Setup again (it's idempotent), then publish again.

Part 3: AWS SQS & SNS — The Serverless Messaging Duo

Overview

A comprehensive demo application showcasing Amazon Simple Queue Service (SQS) and Simple Notification Service (SNS) functionality built with Next.js 16.

Features

SQS (Simple Queue Service)

  • Send Messages: Add messages to your SQS queue
  • Receive Messages: Poll and retrieve messages from the queue
  • Delete Messages: Remove processed messages from the queue
  • Queue Status: Monitor queue attributes and message counts
  • Long Polling: Efficient message retrieval with reduced API calls

SNS (Simple Notification Service)

  • Publish Messages: Send notifications to all subscribers
  • Manage Subscriptions: Add email, SMS, HTTP, and SQS subscriptions
  • Topic Management: View and manage topic subscriptions
  • Message Attributes: Support for custom message attributes
  • Multiple Protocols: Email, SMS, HTTP/HTTPS, and SQS endpoints

Architecture

This demo demonstrates key messaging patterns:

Producer/ConsumerSQS queues for work distribution
Publish/SubscribeSNS topics for event notifications
DecouplingLoose coupling between application components
ScalabilityAsynchronous message processing

Quick Start

1. Install Dependencies

git clone https://github.com/audoir/sqs-sns-tutorial.git
cd sqs-sns-tutorial
npm install

2. Configure AWS Services

Follow the detailed setup guide in AWS_SETUP.md to:

  • • Create IAM user with SQS/SNS permissions
  • • Set up SQS queue
  • • Create SNS topic
  • • Get your AWS credentials

3. Environment Configuration

Create a .env.local file with the following required environment variables:

AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your_access_key_here
AWS_SECRET_ACCESS_KEY=your_secret_key_here
SQS_QUEUE_URL=https://sqs.us-east-1.amazonaws.com/123456789012/your-queue-name
SNS_TOPIC_ARN=arn:aws:sns:us-east-1:123456789012:your-topic-name

4. Run the Application

npm run dev

Open http://localhost:3000 to access the demo interface.

AWS Services Required

IAM Permissions

Your AWS user needs the following permissions:

sqs:SendMessagesqs:ReceiveMessagesqs:DeleteMessagesqs:GetQueueAttributessns:Publishsns:Subscribesns:ListSubscriptionsByTopic
Resources to Create
  1. SQS Queue: Standard or FIFO queue for message processing
  2. SNS Topic: Topic for publishing notifications
  3. IAM User: User with programmatic access and required permissions

API Endpoints

SQS Endpoints
  • POST /api/sqs/send — Send message to queue
  • GET /api/sqs/receive — Receive messages from queue
  • DELETE /api/sqs/delete — Delete message from queue
  • GET /api/sqs/status — Get queue attributes
SNS Endpoints
  • POST /api/sns/publish — Publish message to topic
  • POST /api/sns/subscribe — Create subscription
  • GET /api/sns/subscriptions — List topic subscriptions

Usage Examples

SQS Message Processing

// AWS SDK v3 Configuration
import { SQSClient } from '@aws-sdk/client-sqs';
import { SNSClient } from '@aws-sdk/client-sns';

const awsConfig = {
  region: process.env.AWS_REGION || 'us-east-1',
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
};

export const sqsClient = new SQSClient(awsConfig);
export const snsClient = new SNSClient(awsConfig);
// Send a message to SQS
const response = await fetch('/api/sqs/send', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ message: 'Hello World!' })
});

// Receive messages with long polling
const receiveParams = {
  QueueUrl: process.env.SQS_QUEUE_URL,
  MaxNumberOfMessages: 10,
  WaitTimeSeconds: 20, // Long polling
  MessageAttributeNames: ['All']
};

const messages = await sqsClient.send(
  new ReceiveMessageCommand(receiveParams)
);

// Process each message
for (const message of messages.Messages || []) {
  try {
    await processMessage(JSON.parse(message.Body));
    
    // Delete the message after successful processing
    await sqsClient.send(new DeleteMessageCommand({
      QueueUrl: process.env.SQS_QUEUE_URL,
      ReceiptHandle: message.ReceiptHandle
    }));
  } catch (error) {
    console.error('Message processing failed:', error);
    // Message will become visible again after visibility timeout
  }
}

SNS Notifications

// Publishing a message to SNS topic
const publishParams = {
  TopicArn: process.env.SNS_TOPIC_ARN,
  Message: JSON.stringify({
    event: 'USER_REGISTERED',
    userId: 'user-789',
    email: 'user@example.com',
    timestamp: new Date().toISOString()
  }),
  MessageAttributes: {
    'event_type': {
      DataType: 'String',
      StringValue: 'USER_REGISTERED'
    },
    'priority': {
      DataType: 'String',
      StringValue: 'high'
    }
  }
};

const result = await snsClient.send(new PublishCommand(publishParams));
// Subscribe to topic (via API)
const subscription = await fetch('/api/sns/subscribe', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    protocol: 'email',
    endpoint: 'user@example.com'
  })
});

// Creating subscriptions for different protocols
const subscriptions = [
  { Protocol: 'email',  Endpoint: 'admin@example.com' },
  { Protocol: 'sqs',   Endpoint: 'arn:aws:sqs:us-east-1:123456789012:notification-queue' },
  { Protocol: 'https', Endpoint: 'https://api.example.com/webhooks/notifications' }
];

for (const subscription of subscriptions) {
  await snsClient.send(new SubscribeCommand({
    TopicArn: process.env.SNS_TOPIC_ARN,
    Protocol: subscription.Protocol,
    Endpoint: subscription.Endpoint
  }));
}

Project Structure

├── app/
│   ├── api/
│   │   ├── sqs/          # SQS API endpoints
│   │   │   ├── send/     # Send messages
│   │   │   ├── receive/  # Receive messages
│   │   │   ├── delete/   # Delete messages
│   │   │   └── status/   # Queue status
│   │   └── sns/          # SNS API endpoints
│   │       ├── publish/      # Publish messages
│   │       ├── subscribe/    # Create subscriptions
│   │       └── subscriptions/ # List subscriptions
│   ├── page.tsx          # Main demo interface
│   └── layout.tsx        # App layout
├── components/
│   ├── sqs/
│   │   ├── SQSTab.tsx           # Main SQS tab container
│   │   ├── SendMessage.tsx      # Message sending component
│   │   └── ReceiveMessages.tsx  # Message receiving component
│   ├── sns/
│   │   ├── SNSTab.tsx           # Main SNS tab container
│   │   ├── PublishMessage.tsx   # Message publishing component
│   │   └── ManageSubscriptions.tsx # Subscription management
│   ├── TabNavigation.tsx        # Tab switching component
│   └── SetupInstructions.tsx    # Setup instructions component
├── hooks/
│   ├── useSQS.ts        # SQS state management and API calls
│   └── useSNS.ts        # SNS state management and API calls
├── lib/
│   ├── aws-config.ts    # AWS SDK configuration
│   ├── sqs-service.ts   # SQS service wrapper
│   ├── sns-service.ts   # SNS service wrapper
│   └── types.ts         # TypeScript type definitions
├── AWS_SETUP.md         # Detailed AWS setup guide
└── env.example          # Environment variables template

Custom Hook for SQS Operations

// hooks/useSQS.ts — Custom hook for SQS operations
export function useSQS() {
  const [messages, setMessages] = useState<SQSMessage[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const sendMessage = async (messageBody: string) => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await fetch('/api/sqs/send', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message: messageBody }),
      });
      
      if (!response.ok) throw new Error('Failed to send message');
      return await response.json();
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
      throw err;
    } finally {
      setLoading(false);
    }
  };

  return { messages, loading, error, sendMessage };
}

Troubleshooting

Common Issues
  • Access Denied: Check IAM permissions and AWS credentials
  • Queue/Topic Not Found: Verify ARNs and region configuration
  • Environment Variables: Ensure .env.local is properly configured
  • CORS Issues: API routes handle CORS automatically
Debug Tips
  • • Check browser console for API errors
  • • Verify AWS resources exist in the correct region
  • • Test AWS credentials using AWS CLI: aws sts get-caller-identity
  • • Monitor CloudWatch logs for detailed error information

Next Steps

After exploring this demo:

  • • Implement dead letter queues for failed messages
  • • Add message filtering and routing
  • • Integrate with AWS Lambda for serverless processing
  • • Explore SWF for complex workflow orchestration
  • • Set up CloudWatch monitoring and alarms

Learning Outcomes

By completing all three tutorials, you will have gained hands-on experience with:

Apache Kafka

  • • Starting Kafka with Docker (KRaft mode)
  • • Creating topics and partitions
  • • Producing messages with and without keys
  • • Consumer groups and offset tracking
  • • Partition assignment and rebalancing
  • • At-least-once vs. exactly-once delivery

RabbitMQ

  • • Setting up RabbitMQ with Docker
  • • All four core exchange types
  • • The Setup → Publish → Consume workflow
  • • Dead Letter Queues and TTL expiry
  • • Reading x-death metadata
  • • Building interactive demos with Next.js

AWS SQS & SNS

  • • Configuring SQS queues and SNS topics
  • • Message sending and receiving with error handling
  • • Pub/sub patterns with SNS subscriptions
  • • Integrating AWS SDK v3 with Next.js
  • • Managing AWS credentials and IAM permissions
  • • Fan-out architecture (SNS + SQS)

Conclusion

Kafka, RabbitMQ, SQS, and SNS are all excellent tools — but they excel in different scenarios. Use Kafka when you need massive throughput, event replay, or long-term event storage. Use RabbitMQ when you need sophisticated routing logic, multi-protocol support, or traditional task queues. Use SQS when you want zero-ops simplicity in an AWS environment with reliable point-to-point queuing. Use SNS when you need to broadcast a single event to multiple subscribers or trigger serverless workflows.

Often the most powerful architectures combine these tools — for example, the classic SNS + SQS fan-out pattern, or using Kafka for high-throughput ingestion with RabbitMQ for fine-grained task routing downstream.

About the Author

Wayne Cheng is the founder and AI app developer at Audoir, LLC. Prior to founding Audoir, he worked as a hardware design engineer for Silicon Valley startups and an audio engineer for creative organizations. He holds an MSEE from UC Davis and a Music Technology degree from Foothill College.

Further Exploration

Explore the complete tutorial repositories to experiment with advanced features:

  • Kafka Tutorial — Add stream processing with Kafka Streams or experiment with consumer group rebalancing
  • RabbitMQ Tutorial — Extend the exchange demos with message priorities or RabbitMQ Streams
  • SQS & SNS Tutorial — Implement dead letter queues, message filtering, and Lambda integration

For more AI-powered development tools and tutorials, visit Audoir .