Một API nhận order làm 5 việc đồng bộ: validate payment → charge → update inventory → gửi email → update Elasticsearch. Response time 3 giây, user than “app chậm”, database connection pool cạn vì quá nhiều connection đồng thời.
Giải pháp: decouple bằng message queue. API nhận order → ghi database → đẩy message vào SQS → return 200 OK trong <100ms. Worker đọc SQS xử lý email, inventory, search index sau — async, không block user, không quá tải database.
SQS là service queue lâu đời nhất trên AWS (từ 2004), xử lý hàng tỷ message mỗi ngày cho Amazon.com trước khi thành service public. Bài này đi sâu vào mọi khía cạnh: internal architecture, Standard vs FIFO, visibility timeout mechanics, DLQ strategy, batch processing, long polling, và troubleshooting.
flowchart TB
API["API: POST /orders<br/>return 200 OK trong <100ms"]
SQS["SQS Standard Queue<br/>at-least-once · unlimited throughput"]
Worker["Lambda Worker<br/>batch size: 10 · concurrency: 5"]
DLQ["DLQ<br/>sau 3 lần retry thất bại"]
Email["SES: email confirm"]
Inventory["Update inventory system"]
Search["Elasticsearch index"]
API -->|"SendMessage"| SQS
SQS -->|"poll (long polling 20s)"| Worker
Worker -->|"success → delete"| Email
Worker -->|"success → delete"| Inventory
Worker -->|"success → delete"| Search
Worker -->|"fail × 3"| DLQ
DLQ -->|"redrive sau khi fix bug"| SQS
```text
---
## SQS hoạt động như thế nào?
SQS là **distributed queue** chạy trên hàng nghìn server trong một region. Khi bạn gửi message, SQS:
1. Nhận message → lưu redundant trên **multiple AZs** trong region
2. Return HTTP 200 cho producer (message đã được persisted an toàn)
3. Khi consumer gọi ReceiveMessage, SQS chọn một tập message từ partition nội bộ và trả về
4. Consumer xử lý xong → gọi DeleteMessage → SQS xóa message khỏi tất cả bản sao redundancy
5. Nếu consumer không delete trong visibility timeout → message tự động "xuất hiện lại" để consumer khác xử lý
Điều này có nghĩa:
- **Message được lưu redundant ít nhất 2 AZs** — một AZ sập, message không mất
- **Producer được confirm ngay khi message persisted** — không phải đợi consumer xử lý
- **Consumer có thể fail** — message không mất, tự động retry sau visibility timeout
- **Scale ngang không giới hạn** — SQS tự động thêm partition khi throughput tăng
Mỗi SQS queue có thể chứa **không giới hạn số lượng message**. Retention mặc định 4 ngày, tối đa 14 ngày. Message size tối đa 256KB (dùng S3 + SQS Extended Client Library cho message lớn hơn).
---
## Standard vs FIFO: chọn đúng loại queue
Đây là quyết định thiết kế quan trọng nhất với SQS. Hai loại queue khác nhau về **cơ chế hoạt động**, không chỉ về tính năng.
### Standard Queue: throughput không giới hạn
**Internal architecture:** Standard queue dùng **multiple partitions** phía sau. Mỗi partition xử lý một phần message. Khi bạn gọi ReceiveMessage, SQS query một số partition (không phải tất cả). Điều này có nghĩa:
- **Best-effort ordering:** Message từ partition A có thể đến trước partition B, dù gửi sau → thứ tự không được đảm bảo
- **At-least-once delivery:** Có thể nhận duplicate message (SQS gửi từ partition A, nhưng message cũng được replicated sang partition B trong quá trình redundancy → cả hai partition đều gửi)
- **Unlimited throughput:** Càng nhiều partition → càng nhiều throughput. SQS tự động thêm partition khi traffic tăng
### FIFO Queue: strict ordering, limited throughput
**Internal architecture:** FIFO queue dùng **single partition** (hoặc group-level partition với MessageGroupId). Điều này đảm bảo:
- **Strict ordering:** Message trong cùng group được xử lý theo đúng thứ tự gửi
- **Exactly-once processing:** SQS dùng deduplication ID để đảm bảo không duplicate trong 5 phút window
- **Throughput limit:** 300 msg/s (không batch) hoặc 3,000 msg/s (batch 10). Vì tất cả message cùng group đi qua một partition
| Đặc tính | Standard | FIFO |
|----------|----------|------|
| **Throughput** | Unlimited (auto-scale) | 300/s (single), 3,000/s (batch) |
| **Ordering** | Best-effort | Strict (per MessageGroupId) |
| **Delivery** | At-least-once (có thể duplicate) | Exactly-once (dedup 5 phút) |
| **Partition model** | Multi-partition | Single partition (hoặc per-group) |
| **Dùng khi** | 99% use case: order processing, notification, image resize, async task | Financial transactions, sequential workflows, event sourcing |
| **Cost/1M requests** | $0.40 | $0.50 |
| **Giới hạn tên** | Bất kỳ | Phải kết thúc `.fifo` |
```bash
# Standard queue — dùng 99% thời gian
aws sqs create-queue --queue-name order-processing \
--attributes VisibilityTimeout=300,MessageRetentionPeriod=345600,ReceiveMessageWaitTimeSeconds=20
# FIFO queue
aws sqs create-queue --queue-name order-processing.fifo \
--attributes FifoQueue=true,ContentBasedDeduplication=true,VisibilityTimeout=300
```text
```typescript
// TypeScript: gửi message vào FIFO queue với dedup
await sqs.send(new SendMessageCommand({
QueueUrl: fifoQueueUrl,
MessageBody: JSON.stringify({ orderId: "ORD-001", total: 150000 }),
MessageGroupId: "orders", // Bắt buộc với FIFO
MessageDeduplicationId: "ORD-001-2026-06-21", // Unique trong 5 phút
}));
// Hoặc dùng ContentBasedDeduplication → SQS tự hash body
```text
Bắt đầu với Standard, chỉ chuyển sang FIFO khi thực sự cần ordering. 99% use case có thể tolerate duplicate message (dùng idempotency key ở consumer). Standard có throughput không giới hạn — bạn không bao giờ lo throttle. Với FIFO, 300 msg/s là hard limit — nếu traffic spike vượt ngưỡng, SQS return 400 (ThrottlingException).
---
## Visibility timeout: cơ chế retry
Visibility timeout là **trái tim của SQS retry logic**. Khi consumer nhận message, message bị "ẩn" trong khoảng thời gian này. Nếu consumer:
- **Xử lý thành công + gọi DeleteMessage** → message bị xóa khỏi queue
- **Fail (throw exception)** → message không bị delete. Hết visibility timeout → message "xuất hiện lại" → consumer khác (hoặc consumer cũ) nhận được
- **Crash (process chết)** → giống fail — message tự động retry sau visibility timeout
Mỗi lần message được nhận nhưng không delete, `ApproximateReceiveCount` tăng lên 1. Đây là cơ chế để DLQ biết message đã retry bao nhiêu lần.
### Công thức visibility timeout
```text
visibility_timeout >= (lambda_timeout × 2) + processing_buffer
Ví dụ:
Lambda timeout = 30s
Processing buffer = 10s (network latency, SQS API call overhead)
→ Visibility timeout = 70s (tối thiểu 60s)
```text
**Tại sao phải ×2?** Vì bạn cần buffer cho worst case: Lambda nhận message ở giây 29, bắt đầu xử lý, cần thêm 30s để hoàn thành. Nếu visibility timeout = 30s, message hết timeout khi Lambda đang xử lý → giao cho Lambda khác → duplicate processing.
```bash
aws sqs set-queue-attributes --queue-url $QUEUE_URL \
--attributes VisibilityTimeout=120
```text
### Dynamic visibility timeout extension
Nếu processing time không dự đoán được (ví dụ: video encoding 5-10 phút), bạn có thể **extend visibility timeout từ consumer**:
```typescript
import { SQSClient, ChangeMessageVisibilityCommand } from "@aws-sdk/client-sqs";
async function processMessageWithExtension(
sqs: SQSClient,
queueUrl: string,
receiptHandle: string,
message: string
) {
// Bắt đầu xử lý
const startTime = Date.now();
// Extend visibility timeout sau mỗi 30s nếu vẫn đang xử lý
const heartbeat = setInterval(async () => {
await sqs.send(new ChangeMessageVisibilityCommand({
QueueUrl: queueUrl,
ReceiptHandle: receiptHandle,
VisibilityTimeout: 60, // Extend thêm 60s
}));
console.log(`Heartbeat: ${Date.now() - startTime}ms elapsed`);
}, 30_000);
try {
await processVideo(message); // Có thể mất 5-10 phút
clearInterval(heartbeat);
// Lambda tự động delete message khi return success
} catch (error) {
clearInterval(heartbeat);
throw error; // Retry
}
}
```text
---
## Dead Letter Queue: xử lý message "chết"
DLQ là nơi message đến sau khi retry thất bại N lần. Đây là **safety net** — thay vì message bị mất hoặc loop retry vô hạn, nó được chuyển vào DLQ để bạn điều tra sau.
### Cấu hình DLQ
```bash
# 1. Tạo DLQ (Standard queue)
aws sqs create-queue --queue-name order-processing-dlq
# 2. Gán DLQ cho main queue
aws sqs set-queue-attributes --queue-url $QUEUE_URL --attributes '{
"RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:ap-southeast-1:123456789012:order-processing-dlq\",\"maxReceiveCount\":\"3\"}"
}'
```text
`maxReceiveCount` nên là bao nhiêu?
| maxReceiveCount | Behavior | Dùng khi |
|-----------------|----------|----------|
| 1 | Không retry — fail 1 lần → DLQ | Non-critical notification |
| 3 | Retry 2 lần → DLQ (recommended) | Hầu hết use case |
| 5 | Retry 4 lần → DLQ | Critical business logic, transient errors thường xuyên |
| 10+ | Retry nhiều → DLQ | External dependency không ổn định |
```typescript
// CDK: SQS queue + DLQ trong vài dòng
const dlq = new sqs.Queue(this, "DLQ", {
retentionPeriod: cdk.Duration.days(14), // Giữ message 14 ngày để điều tra
});
const mainQueue = new sqs.Queue(this, "MainQueue", {
visibilityTimeout: cdk.Duration.seconds(120),
deadLetterQueue: {
queue: dlq,
maxReceiveCount: 3,
},
});
```text
### Redrive: gửi message từ DLQ về main queue
Sau khi fix bug khiến message fail, bạn muốn xử lý lại tất cả message đang nằm trong DLQ:
```bash
# Redrive toàn bộ DLQ → main queue
aws sqs start-message-move-task \
--source-arn arn:aws:sqs:ap-southeast-1:123456789012:order-processing-dlq \
--destination-arn arn:aws:sqs:ap-southeast-1:123456789012:order-processing
```text
**Quy trình chuẩn khi có message trong DLQ:**
1. CloudWatch Alarm: `ApproximateNumberOfMessagesVisible` trong DLQ > 0 → alert
2. Kiểm tra DLQ message: đọc sample message → tìm pattern lỗi
3. Fix bug (code hoặc config)
4. Redrive message từ DLQ về main queue
5. Monitor: message được xử lý thành công lần này?
---
## Lambda SQS Event Source: batch + concurrency
```typescript
// CDK: Lambda SQS trigger với tối ưu batch
handler.addEventSource(new SqsEventSource(queue, {
batchSize: 10, // 10 messages/batch
maxBatchingWindow: cdk.Duration.seconds(30), // Chờ 30s để gom đủ batch
reportBatchItemFailures: true, // Partial batch failure
maxConcurrency: 5, // Tối đa 5 Lambda đồng thời
}));
```text
### Lambda handler: xử lý batch với partial failure
```typescript
import { SQSEvent, SQSRecord } from "aws-lambda";
export const handler = async (event: SQSEvent) => {
const batchItemFailures: { itemIdentifier: string }[] = [];
for (const record of event.Records) {
try {
const body = JSON.parse(record.body);
const retryCount = parseInt(record.attributes.ApproximateReceiveCount || "1");
console.log(`Processing message ${record.messageId}, retry #${retryCount}`);
// Idempotency check: nếu đã xử lý rồi → skip
if (await isAlreadyProcessed(body.orderId)) {
console.log(`Order ${body.orderId} already processed — skipping`);
continue; // Thành công, không retry
}
await processOrder(body);
await markAsProcessed(body.orderId); // Idempotency marker
} catch (error) {
console.error(`Failed to process message ${record.messageId}:`, error);
// Chỉ retry nếu là transient error (DB timeout, network)
if (isTransientError(error)) {
batchItemFailures.push({ itemIdentifier: record.messageId });
}
// Nếu là permanent error (invalid data) → không retry, log + alert
}
}
return { batchItemFailures };
};
```text
### Tại sao partial batch failure quan trọng?
Không có `reportBatchItemFailures`: 1 message fail → cả batch bị retry. 9 message đã xử lý thành công bị duplicate processing.
Có `reportBatchItemFailures`: chỉ message fail bị retry. 9 message thành công bị xóa khỏi queue.
---
## Long polling: tiết kiệm request, giảm cost
### Short polling (mặc định nếu không config)
```text
Consumer → ReceiveMessage → SQS check partition →
Có message → return ngay
KHÔNG có message → return empty response ngay
Vấn đề: empty response vẫn tính là 1 request. Nếu poll mỗi 100ms
và không có message trong 1 giờ → 36,000 request vô ích → lãng phí cost
```text
### Long polling (ReceiveMessageWaitTimeSeconds > 0)
```text
Consumer → ReceiveMessage → SQS check partition →
Có message → return ngay
KHÔNG có message → GIỮ KẾT NỐI MỞ, chờ tối đa 20s
Trong 20s, có message mới → return ngay
Sau 20s không có → return empty response
```text
```bash
aws sqs set-queue-attributes --queue-url $QUEUE_URL \
--attributes ReceiveMessageWaitTimeSeconds=20
```text
Lambda SQS trigger **mặc định dùng long polling**. Bạn không cần config gì thêm.
---
## Troubleshooting SQS
Mình đã từng gặp case visibility timeout quá ngắn, message được xử lý 4 lần trước khi kịp delete, khách hàng nhận 4 email — từ đó mình luôn set visibility timeout gấp 3 lần Lambda timeout.
### Problem 1: Message bị xử lý duplicate
**Triệu chứng:** Order bị charge 2 lần, email confirm gửi 2 lần.
**Nguyên nhân:** Visibility timeout < Lambda timeout. Lambda A đang xử lý, message hết timeout → Lambda B nhận cùng message → cả hai cùng xử lý.
**Fix:**
```bash
# Tăng visibility timeout lên ít nhất 2-3x Lambda timeout
aws sqs set-queue-attributes --queue-url $QUEUE_URL --attributes VisibilityTimeout=120
# Implement idempotency trong consumer
await processOrderIfNotAlreadyProcessed(orderId);
```text
### Problem 2: DLQ đầy nhưng message biến mất
**Triệu chứng:** Message biến mất khỏi DLQ sau vài ngày.
**Nguyên nhân:** DLQ retention period = main queue retention period. Message expire.
**Fix:**
```bash
# DLQ retention nên dài hơn main queue (14 ngày)
aws sqs set-queue-attributes --queue-url $DLQ_URL --attributes MessageRetentionPeriod=1209600
```text
### Problem 3: Lambda bị throttle, SQS message tồn đọng
**Triệu chứng:** SQS queue depth tăng đều, Lambda throttle metric tăng.
**Nguyên nhân:** Lambda reserved concurrency quá thấp so với SQS message rate.
**Fix:**
```bash
# Tăng Lambda reserved concurrency
aws lambda put-function-concurrency --function-name sqs-worker --reserved-concurrent-executions 50
# Hoặc tăng SQS event source maxConcurrency (CDK)
handler.addEventSource(new SqsEventSource(queue, {
maxConcurrency: 20, // Cho phép 20 Lambda concurrent
}));
```text
### Problem 4: FIFO queue throttling
**Triệu chứng:** ThrottlingException (HTTP 400) khi gửi message vào FIFO queue lúc traffic cao.
**Nguyên nhân:** FIFO queue limit 300 msg/s (hoặc 3,000/s batch). Spike vượt ngưỡng.
**Fix:**
```bash
# 1. Dùng batch SendMessageBatch (tăng throughput lên 3,000/s)
aws sqs send-message-batch --queue-url $FIFO_Q_URL --entries [...]
# 2. High throughput FIFO (có sẵn cho queue tạo sau 11/2019; chỉ không thể thay đổi DeduplicationScope/FifoThroughputLimit nếu queue đã từng có message)
# Queue tự động chuyển sang high throughput mode sau khi enable
# 3. Nếu vẫn không đủ → chuyển sang Standard queue + idempotency
```text
---
## Cost optimization
| Item | Cost | Tối ưu |
|------|------|--------|
| **API Requests** | $0.40/1M (Standard) | Batch SendMessage + batch DeleteMessage |
| **Empty receives** | Vẫn tính 1 request | Long polling (20s) — giảm 50-90% empty receives |
| **Data transfer** | Free (cùng region), $0.01-0.02/GB (cross-region) | Giữ queue và consumer cùng region |
| **S3 for large messages** | S3 cost + SQS request cost | Chỉ dùng Extended Client Library khi message > 256KB |
---
SQS là service đơn giản về concept nhưng sâu về implementation. Những điểm cốt lõi:
- **Standard cho 99% use case** — unlimited throughput, at-least-once. FIFO cho strict ordering với cost = throughput limit
- **Visibility timeout là cơ chế retry** — luôn set > 2-3x Lambda timeout. Dynamic extension cho long-running task
- **DLQ + maxReceiveCount = 3** — message fail 3 lần → DLQ → alert → điều tra → fix → redrive
- **Partial batch failure** (`reportBatchItemFailures: true`) — chỉ retry message lỗi, không retry cả batch
- **Long polling (20s)** — giảm empty receive, giảm cost, mặc định với Lambda
- **Idempotency ở consumer** — at-least-once có nghĩa là message CÓ THỂ bị duplicate. Luôn implement idempotency check
- **SQS + Lambda là cặp hoàn hảo** cho async processing: auto-scale, pay-per-use, zero ops
Bài sau: [Phần 26: SNS — pub/sub & fan-out pattern](/posts/aws/26-sns-pubsub-fanout-pattern/)
## Câu hỏi hay gặp
**Q: Standard queue duplicate message bao nhiêu phần trăm?**
A: AWS không công bố con số chính xác. Trong thực tế, duplicate cực hiếm (<0.1%) với queue bình thường, nhưng có thể tăng khi có network partition hoặc AZ failure. Luôn implement idempotency.
**Q: SQS có đảm bảo message không bị mất không?**
A: Message được lưu redundant trên multiple AZs. Một AZ sập → message vẫn tồn tại. Nhưng nếu cả region sập → message trong queue có thể bị mất (extremely rare). Dùng multi-region replication nếu cần zero data loss.
**Q: Làm sao biết message nào đang gây lỗi trong DLQ?**
A: Đọc sample message từ DLQ console hoặc CLI. Tạo CloudWatch alarm cho `ApproximateNumberOfMessagesVisible` trong DLQ. Setup Lambda trigger cho DLQ để tự động parse và log error pattern.