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.
Kafka Tutorial
Interactive Kafka app built with Next.js and KafkaJS. Create topics, produce messages, consume them, and inspect consumer groups.
View on GitHubRabbitMQ Tutorial
Interactive demos for all five exchange types built with Next.js and amqplib. Fanout, Direct, Topic, Headers, and Dead Letter Queues.
View on GitHubSQS & 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 GitHubTable of Contents
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:
| Feature | Apache Kafka | RabbitMQ | AWS SQS | AWS SNS |
|---|---|---|---|---|
| Primary Use Case | Massive event streaming, log aggregation, event replay | Complex message routing, traditional task queues | Simple serverless task queues, buffering/load-leveling | Broadcasting messages, Fan-out, User notifications |
| Architecture | Distributed Log / Append-only | Message Broker (Exchanges & Queues) | Simple Point-to-Point Queue | Pub/Sub (Push based) |
| Message Retention | Days, weeks, or indefinitely | Deleted immediately after consumption (Ack) | Up to 14 days | No retention (fire-and-forget unless queued) |
| Consumer Model | Pull (via Offsets) | Push (default) or Pull | Pull (Long-polling) | Push (to HTTP, Lambda, SQS, Email, SMS) |
| Replayability | ✅ Yes (Native offset resetting) | ❌ No (Once consumed, it's gone) | ❌ No | ❌ No |
| Routing Logic | Very basic (Topics/Partitions) | Highly Advanced (Regex, Headers, Routing Keys) | None (1 Queue = 1 destination) | Basic (Subscription filters) |
| Overhead/Ops | High (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: MkU3OEVBNTcwNTJENDM2QkThen 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
- 1Clone the repository:
git clone https://github.com/audoir/kafka-tutorial.git cd kafka-tutorial - 2Install dependencies:
npm install - 3Start 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:
| Concept | Key Takeaway |
|---|---|
| Topics & Partitions | A topic is a named stream split into partitions. Each message gets an immutable offset per partition. |
| Producers & Message Keys | No key → round-robin across partitions. With a key → same key always lands on the same partition (ordering guarantee). |
| Consumers & Deserialization | Pull model. Reads messages in offset order within each partition. |
| Consumer Groups & Offsets | A group shares partitions among its consumers. Kafka tracks progress in __consumer_offsets. |
| Brokers & Replication | A 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.
- Enter a name like
demo-topic, set Partitions to3, leave Replication Factor at1, click Create Topic - Click 🔄 Refresh to fetch all topics — you'll see your new topic along with internal topics like
__consumer_offsets - 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-band 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.
| Scenario | What happens |
|---|---|
| 3 partitions, 3 consumers | Perfect — one partition per consumer |
| 3 partitions, 2 consumers | Consumer A gets 2 partitions, Consumer B gets 1 |
| 3 partitions, 4 consumers | Consumer D is idle — more consumers than partitions |
| Multiple groups on same topic | Each 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.
| Route | Description |
|---|---|
| GET /api/health | Calls admin.describeCluster() to verify the broker is reachable. Returns cluster ID, controller ID, and broker list. Returns HTTP 503 on failure. |
| GET /api/topics | Lists all topics via admin.listTopics(), then fetches partition metadata (leader, replicas, ISR). |
| POST /api/topics | Creates a topic. Body: { topic, numPartitions, replicationFactor } |
| DELETE /api/topics | Deletes a topic. Body: { topic } |
| POST /api/produce | Sends one or more messages to a topic. Returns delivery receipts (partition + baseOffset per message). |
| POST /api/consume | Subscribes a consumer to a topic, collects messages for up to 3 seconds, then disconnects. Returns { partition, offset, key, value, timestamp } objects. |
| GET /api/consumer-groups | Lists 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 downPart 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:managementOption 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:managementOption 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 -dOpen 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 rabbitmq2. Set Up the Next.js App
- 1Clone the repository:
git clone https://github.com/audoir/rabbitmq-tutorial.git - 2Install dependencies:
npm install - 3Start 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:56724. How the Code is Structured
Each exchange type follows a strict Setup → Publish → Consume pattern:
| Route | Responsibility |
|---|---|
| setup/route.ts | Creates the exchange, queues, and bindings. Must be called first. |
| publish/route.ts | Publishes a message to the exchange. Assumes setup has been done. |
| consume/route.ts | Reads 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:
- Open
http://localhost:3000/tutorial/fanout - Click Setup Exchange & Queues
- Click Publish to send a message
- Consume from
fanout.queue.1— you get the message - 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 exchangesStep 2 — Direct Exchange (/tutorial/direct)
A direct exchange routes messages to queues whose binding key exactly matches the routing key.
Try it:
- Open
http://localhost:3000/tutorial/direct - Click Setup Exchange & Queues
- Publish with routing key
error - Consume with routing key
error→ you get the message ✓ - 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 messageStep 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:
- Open
http://localhost:3000/tutorial/topic - Click Setup Exchange & Queues
- Publish with key
logs.error - Publish with key
logs.error.critical - Consume with pattern
logs.*→ gets onlylogs.error(one word after logs) - 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:
- Open
http://localhost:3000/tutorial/headers - Click Setup Exchange & Queues
- Publish with headers
{"format":"pdf","type":"report"} - Consume with
x-match=alland headers{"format":"pdf","type":"report"}→ match ✓ - Consume with
x-match=alland headers{"format":"pdf","type":"invoice"}→ no match ✗ - Consume with
x-match=anyand 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):
- Open
http://localhost:3000/tutorial/deadletter - Click Setup Exchanges & Queues
- Click Publish to Main Queue
- Click Reject Message (NACK) — simulates a failed consumer
- Click Consume from DLQ — see the rejected message with
x-deathmetadata
Try it (TTL expiry):
- After setup, publish with TTL = 3000ms
- Wait 3 seconds (don't reject)
- 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 / closeConnection7. Exchange Type Quick Reference
| Type | Routing Logic | Routing Key? | Best For |
|---|---|---|---|
| Fanout | All bound queues | ❌ Ignored | Broadcasting, notifications |
| Direct | Exact key match | ✅ Exact match | Log levels, task routing |
| Topic | Pattern matching | ✅ Wildcards (* #) | Flexible routing, microservices |
| Headers | Header attributes | ❌ Ignored | Attribute-based routing |
8. RabbitMQ Concepts
- •
durable— survives broker restart - •
auto-delete— deleted when last consumer disconnects - •
exclusive— only accessible by the declaring connection
x-death):- •
queue— original queue name - •
reason—rejected,expired, ormaxlen - •
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
- • Make sure Docker is running:
docker ps | grep rabbitmq - • Start RabbitMQ:
docker start rabbitmq - • Check logs:
docker logs rabbitmq
# 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- • Go to the Management UI → Queues → delete the conflicting queue
- • Or restart RabbitMQ:
docker restart rabbitmq
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:
Quick Start
1. Install Dependencies
git clone https://github.com/audoir/sqs-sns-tutorial.git
cd sqs-sns-tutorial
npm install2. 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-name4. Run the Application
npm run devOpen http://localhost:3000 to access the demo interface.
AWS Services Required
Your AWS user needs the following permissions:
sqs:SendMessagesqs:ReceiveMessagesqs:DeleteMessagesqs:GetQueueAttributessns:Publishsns:Subscribesns:ListSubscriptionsByTopic- SQS Queue: Standard or FIFO queue for message processing
- SNS Topic: Topic for publishing notifications
- IAM User: User with programmatic access and required permissions
API Endpoints
POST /api/sqs/send— Send message to queueGET /api/sqs/receive— Receive messages from queueDELETE /api/sqs/delete— Delete message from queueGET /api/sqs/status— Get queue attributes
POST /api/sns/publish— Publish message to topicPOST /api/sns/subscribe— Create subscriptionGET /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 templateCustom 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
- • Access Denied: Check IAM permissions and AWS credentials
- • Queue/Topic Not Found: Verify ARNs and region configuration
- • Environment Variables: Ensure
.env.localis properly configured - • CORS Issues: API routes handle CORS automatically
- • 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 .