Skip to main content

Dead Letter Queue (DLQ) in System Design: Complete Guide to Failed Messages

Calculating read time…

Imagine a postal worker delivering letters all day. Most letters reach their destination just fine. But every now and then, one letter has an unreadable address. The mailbox is full. The recipient has moved. The building no longer exists.

The postal worker can't deliver it. They try again tomorrow. And the day after. At some point, someone has to make a decision: where does this letter go? You can't keep trying forever. You can't throw it away — it might be important.

That's exactly the problem that led engineers to invent the Dead Letter Queue — a dedicated holding area for messages that your system failed to process after multiple attempts. It is one of the most important reliability patterns in all of distributed systems.




💡 DLQ Powers Production at Scale

☁️ AWS SQS DLQ: Every production SQS queue should have one — period
📨 Apache Kafka Dead Letter Topics: Custom pattern used by Netflix, Uber, LinkedIn
🐰 RabbitMQ Dead Letter Exchange (DLX): Built-in routing for failed messages
🔵 Azure Service Bus: Native dead-letter subqueue on every topic and queue
🌱 Spring Cloud Stream: Automatic DLQ routing for microservices
📦 Google Cloud Pub/Sub: Dead-letter topics with configurable max delivery attempts

If your system uses message queues and doesn't have a DLQ strategy, you are one bad message away from a silent production outage.

📮 Section 1: Think of DLQ Like the Post Office's Undeliverable Mail Room

Every post office in the world has a special room called the Dead Letter Office (that's literally where the name comes from!). It's where all the letters go that couldn't be delivered for any reason: wrong address, damaged package, recipient unknown, no return address.

The post office doesn't throw these away. They don't keep trying forever either. They route them to a central place where specialists can inspect them, find the real recipient, or contact the sender. Everything is logged. Nothing is silently discarded.

A Dead Letter Queue works exactly the same way — for software messages.

📮 Post Office Analogy 💀 DLQ Reality 🔧 Engineering Concept
Undeliverable letter Message consumer keeps throwing an exception Poison pill message
Try re-delivery 3 times Retry message up to maxReceiveCount Max retry / receive count
Route to Dead Letter Office Move message to DLQ after retries exhausted Redrive policy / DLQ routing
Specialists inspect failed letters Engineers examine failed messages + fix bugs DLQ monitoring + analysis
Re-deliver once address is found Replay messages from DLQ after fixing the bug DLQ replay / redrive

🔥 Section 2: The Core Problem — The Infinite Retry Loop of Death

To understand why DLQs exist, you need to understand the nightmare scenario that happens without them.

💡 The Poison Pill Problem

A "poison pill" message is a message that consistently causes your consumer to crash or throw an exception — no matter how many times it is retried.

Think of it like a corrupt file. Every application that tries to open it crashes. You could retry opening it a million times — it would crash every single time. The file is fundamentally broken (or your application can't handle it).

Now imagine this poison pill sitting at the front of your message queue. Without a DLQ, the queue will retry it forever, keeping all subsequent messages blocked. Your entire processing pipeline grinds to a halt. This is the infinite retry loop of death. 💀

☠️ Without DLQ — The Infinite Retry Nightmare

📬 Queue:
☠️ MSG #1
(corrupt JSON)
📦 MSG #2
(valid)
📦 MSG #3
(valid)
📦 MSG #4
(valid)
... hundreds more ...
☠️ MSG #1
→
🖥️ Consumer
→
💥 EXCEPTION! Return to queue. Retry 1/∞
☠️ MSG #1 (again!)
→
🖥️ Consumer
→
💥 EXCEPTION! Return to queue. Retry 2/∞
☠️ MSG #1 (STILL again!)
→
💀 MSG #2, #3, #4... ALL BLOCKED FOREVER. Pipeline dead.
😱 What Actually Happens: The poison pill message blocks the entire pipeline. All valid messages behind it are stuck. Customers don't get their emails. Orders aren't processed. Notifications never fire. Your on-call engineer gets paged at 3 AM. The only fix without DLQ: manually delete the bad message from the queue.

✅ WITH DLQ — Poison Pill is Safely Isolated

☠️ MSG #1
→ Consumer → 💥 fail → retry 1
☠️ MSG #1
→ Consumer → 💥 fail → retry 2
☠️ MSG #1
→ Consumer → 💥 fail → retry 3 (MAX reached!)
☠️ MSG #1
⟹
💀 DEAD LETTER QUEUE
🎉 ISOLATED! Pipeline unblocked!
📦 MSG #2
→
🖥️ Consumer
→
✅ SUCCESS! Processing continues!

⚙️ Section 3: The Complete Message Lifecycle — From Queue to DLQ

Let's trace the full journey of a message — from the moment it enters a queue to its final destination in the DLQ.

🔄 Message Lifecycle — From Producer to DLQ

1
Producer publishes a message to the Main Queue
An order service sends a message: "Process payment for order #7829." The message enters the main queue with a visibility timeout and delivery count = 0.
⬇️
2
Consumer picks up the message and tries to process it
The payment service receives the message. The message is now "in-flight" — invisible to other consumers for the duration of the visibility timeout (e.g. 30 seconds). Delivery count increments to 1.
⬇️ (if processing fails)
3
Consumer fails — message becomes visible again (retry)
The consumer throws an exception. It does NOT acknowledge (delete) the message. After the visibility timeout expires, the message becomes visible again in the main queue. Delivery count = 2. The next available consumer picks it up and tries again.
⬇️ (repeats N times)
4
⚠️ Max Receive Count Reached — Message Moved to DLQ
After N failures (configurable — e.g. 3 or 5), the queue infrastructure automatically moves the message to the Dead Letter Queue. This is called the redrive policy. The message is no longer in the main queue — the pipeline is unblocked!
⬇️
5
💀 Message sits in DLQ — Alerts fire, engineers are notified
The DLQ depth increases. A CloudWatch alarm (or equivalent) fires. PagerDuty/Slack alert goes to the on-call engineer. The message is safely preserved in the DLQ with full metadata: original queue name, failure reason, timestamp, and original message body.
⬇️ (after bug fix)
6
✅ Engineer fixes the bug → Replays messages from DLQ
Once the root cause is fixed (bad consumer code, schema mismatch, downstream outage), the engineer moves messages from DLQ back to the main queue for reprocessing. Now they process successfully. Nothing is lost. Business continues.

❓ Section 4: Why Do Messages End Up in the DLQ?

Understanding the root causes of DLQ messages is essential for building resilient systems. Messages land in the DLQ for several distinct reasons — each requiring a different fix.

🐛
1. Bug in Consumer Code

A NullPointerException. An unhandled edge case. A division by zero. The most common cause. A code bug causes the consumer to throw an exception every time it touches a specific message. Fix: deploy a bug fix, then replay from DLQ.

💔
2. Bad / Malformed Message (Poison Pill)

Corrupt JSON. Wrong field types. Missing required fields. Schema version mismatch. The message itself is broken — no consumer can ever process it correctly. Fix: fix the producer to send valid messages. The DLQ message may need manual correction.

🔌
3. Downstream Service Outage

The consumer calls a database or API that is temporarily unavailable. All messages fail during the outage. Once the outage is resolved, messages in the DLQ can be replayed successfully. Fix: restore downstream service, then replay from DLQ.

⏱️
4. Consumer Too Slow (Visibility Timeout)

Processing takes longer than the queue's visibility timeout. The message becomes visible again while still being processed — it's delivered to another consumer, processed twice, then delivery count exceeds max. Fix: increase visibility timeout or optimise consumer processing time.

📋
5. Message Too Large for Consumer

A message contains an unexpectedly large payload. Consumer runs out of memory trying to deserialize it. Or a business rule rejects the message size. Fix: implement payload size limits at producer level. Handle large payloads with S3 pointer pattern.

🔍 DLQ Triage Guide — How to Diagnose the Root Cause

All messages fail: Likely a consumer bug or downstream outage. Check consumer logs. Check if dependent services are healthy.
Only specific messages fail: Likely a poison pill / bad message format. Inspect the failing messages. Check if producer changed schema.
Failures correlate with a time window: Likely downstream outage. Check timestamps in DLQ. Cross-reference with incident timeline.
Intermittent failures: Likely race condition or timeout issue. Check visibility timeout vs actual processing time. Check for concurrency bugs.

☁️ Section 5: DLQ in AWS SQS — The Most Common Implementation

AWS SQS has the most widely-used DLQ implementation. If you use SQS in production, understanding every aspect of SQS DLQ is non-negotiable.

☁️ AWS SQS + DLQ Architecture

🖥️ Producer
(Order Service)
➡️
📬 Main SQS Queue
maxReceiveCount = 3
VisibilityTimeout = 30s
➡️
🖥️ Consumer
(Payment Service)
After 3 fails
⬇️
💀 DLQ
Redrive Policy
Retention: 14 days
➡️
🔔 CloudWatch
Alarm when
depth > 0
⚙️ SQS DLQ Concept 📝 What It Means 💡 Recommended Setting
maxReceiveCount How many times a message can be received before moving to DLQ 3–5 for most use cases. Lower for fast-fail. Higher for flaky networks.
VisibilityTimeout How long message is invisible after being received (processing window) 6× your average processing time minimum
MessageRetentionPeriod How long messages stay in the DLQ before being auto-deleted 14 days (max) — don't let messages expire before you fix the bug!
Redrive Policy JSON config linking main queue to DLQ and setting maxReceiveCount Configure at queue creation — easy to miss if added later!
DLQ Redrive (replay) AWS API to move messages from DLQ back to main queue StartMessageMoveTask API — always fix the bug first!
🚫 Critical Mistake: DLQ With a Short Retention Period

If your DLQ has a retention period of 4 days (SQS default) and your team takes 5 days to notice and fix the issue — all DLQ messages are silently deleted. That means lost orders, unprocessed payments, missing notifications. Gone forever.

Always set DLQ retention to the maximum: 14 days in SQS. And alert immediately when DLQ depth exceeds 0. Don't let messages expire before you have a chance to replay them!

📨 Section 6: Dead Letter Topics in Apache Kafka

Unlike SQS and RabbitMQ, Kafka does not have a built-in DLQ mechanism. But Kafka-based systems need dead letter handling just as much. The solution: build it yourself using a Dead Letter Topic (DLT).

💡 Why Kafka Doesn't Have a Native DLQ

Kafka's design philosophy is different from SQS/RabbitMQ. In Kafka, messages are immutable — they can't be "moved" between topics the way they can in SQS. Kafka's retry/DLT pattern is implemented at the consumer application level, not the broker level.

The standard pattern: when your consumer fails, catch the exception and produce the failing message to a dedicated DLT topic (e.g., orders.DLT). Commit the offset so Kafka moves forward. The DLT topic is processed by a separate consumer.

📨 Kafka Dead Letter Topic Pattern — Animated Flow

📨 Topic:
orders
→
📦
→
🖥️ Consumer
(Payment Service)
✅ Success path:
Process → Commit offset → Done! ✅
💥 Failure path:
Exception → catch → produce to DLT → Commit offset → Move on
💀 Topic:
orders.DLT
→
💀
→
🔔 DLT Consumer
(Alert + Log + Analyze)

↑ Spring Cloud Stream / Spring Kafka automatically implements this pattern with @RetryableTopic and dead-letter topic routing. Main consumer always moves forward — no blocking!

🔄 Kafka Retry Topics — Exponential Backoff Before DLT

A popular pattern uses multiple retry topics with increasing delays before the final Dead Letter Topic. This gives transient failures (e.g. a brief DB outage) time to resolve before giving up.

📨 orders
(main)
→ fail →
🔄 orders
-retry-1
wait 1s
→ fail →
🔄 orders
-retry-2
wait 2s
→ fail →
🔄 orders
-retry-3
wait 5s
→ fail →
💀 orders
.DLT
final resting place

Each retry topic has a consumer that waits the specified delay before re-attempting. Only truly unrecoverable failures reach the DLT. Transient failures resolve in retry-1 or retry-2.


🐰 Section 7: Dead Letter Exchange in RabbitMQ

RabbitMQ implements dead lettering through its Dead Letter Exchange (DLX) — a built-in routing mechanism for rejected, expired, or overflowing messages.

🐰 RabbitMQ DLX — Three Ways a Message Gets Dead-Lettered

1. Message Rejected (basic.reject / basic.nack with requeue=false)
Consumer explicitly rejects the message without requeueing it. RabbitMQ routes it to the configured Dead Letter Exchange.
2. Message TTL Expired
If a queue has x-message-ttl set, messages that sit unprocessed longer than the TTL are dead-lettered. Useful for time-sensitive messages (e.g. OTPs, notifications).
3. Queue Length Limit Exceeded
If the queue has x-max-length and is full, oldest messages are dead-lettered to make room for new ones. Prevents unbounded queue growth.
📨 orders_queue
x-dead-letter-exchange:
"dlx.exchange"
→
📤 dlx.exchange
(Dead Letter Exchange)
→
💀 orders_dlq
(Dead Letter Queue)

🔔 Section 8: Monitoring DLQs — The Alert You Must Never Miss

A DLQ with no monitoring is almost as bad as having no DLQ. Messages sit in the DLQ silently while the business loses money. Every DLQ must have an alert on it. No exceptions.

💡 The Bank Vault Alarm Analogy

Imagine a bank vault where you store critical items (your DLQ messages). If someone puts something in that vault unexpectedly (a message fails and lands in the DLQ), you want an alarm to go off immediately — not 3 days later when someone happens to check.

Your DLQ depth should ideally be zero at all times. The moment any message lands in your DLQ, something is wrong. Alert on it. Investigate. Fix it. Clear it.

📊 DLQ Depth Growing Over Time — When to Alert

Depth = 0
✅ All good. No alert needed.
Depth = 1
⚠️ ALERT! Something failed. Investigate NOW.
Depth = 10
🚨 CRITICAL! Systematic failure. Page on-call immediately.
Depth = 100+
💀 INCIDENT! All hands on deck. Downstream outage likely.
📊 Metric to Monitor 🔔 Alert Threshold 🛠️ Tool (AWS)
ApproximateNumberOfMessagesVisible > 0 for DLQ → CRITICAL alert CloudWatch Alarm → SNS → PagerDuty/Slack
ApproximateAgeOfOldestMessage > 1 hour → WARNING (message sitting too long) CloudWatch Alarm → SNS
Main queue consumer errors/sec Spike in errors → pre-DLQ warning Application logs → CloudWatch Logs Insights
DLQ message attributes (failure reason) Group by failure type for root cause analysis Custom CloudWatch dashboard + Lambda DLQ processor

⏪ Section 9: Replaying from the DLQ — Getting Your Messages Back

You've fixed the bug. The downstream service is back up. Now what? You need to process all those messages that failed and are sitting in the DLQ. This is called DLQ Replay — and doing it safely is an art.

⏪ Safe DLQ Replay — Step by Step Checklist

☑️
Step 1 — Verify the root cause is fixed and deployed. Do NOT replay before fixing the bug. You'll just re-populate the DLQ and waste effort.
☑️
Step 2 — Understand the message contents. Read a sample of DLQ messages. Check timestamps. Are they still relevant? A 7-day-old "send OTP" message is probably no longer needed — discard it instead of replaying.
☑️
Step 3 — Check for idempotency. If replaying a payment message, will it charge the customer again? Make sure your consumer is idempotent (see Idempotency blog post!) before replaying.
☑️
Step 4 — Replay gradually (throttle the replay rate). Don't dump 100,000 DLQ messages into the main queue at once. This can overwhelm your downstream services. Replay in batches: 100 messages, check results, then replay more.
☑️
Step 5 — Monitor during replay. Watch your main queue consumer error rate. If errors spike again → stop the replay. The root cause may not be fully fixed yet.
☑️
Step 6 — Confirm DLQ is empty after replay. DLQ depth should return to zero. If not, some messages still can't be processed — inspect those individually.

⏪ Animated: Messages Replaying from DLQ Back to Main Queue

💀 DLQ
5 messages
📦
📬 Main Queue
(Bug Fixed ✅)
→
🖥️ Consumer
Processes ✅

↑ In AWS SQS, use StartMessageMoveTask API to replay. In Kafka, replay by resetting consumer group offsets on the DLT topic.


🏗️ Section 10: Advanced DLQ Patterns

📊 Pattern 1: The DLQ Processor Service

Instead of leaving DLQ messages dormant until an engineer notices, build a dedicated DLQ Processor Service that actively consumes from the DLQ.

DLQ Processor Responsibilities:
📧 Send alert email/Slack with message details
📝 Log full message body to structured logs
☁️ Archive to S3 for long-term retention
📊 Update metrics (failure counts by type)
🎫 Auto-create incident ticket in Jira/PagerDuty

🗂️ Pattern 2: DLQ Archiving to S3

DLQ messages have a maximum retention period (14 days in SQS). If you don't process them in time, they disappear forever. Archive every DLQ message to S3 immediately for long-term analysis.

☁️
S3 Archiving Pattern

DLQ message arrives → DLQ Processor writes to: s3://your-bucket/dlq-archive/{queue-name}/{YYYY}/{MM}/{DD}/{message-id}.json.
Message body + metadata (failure reason, timestamps, retry count) all preserved. Retention: indefinite. Cost: near zero for cold storage. Now you can analyse failures weeks later without losing anything.

🔁 Pattern 3: Automatic Retry with Exponential Backoff

Don't send all retries immediately. Use exponential backoff: wait 1s after failure 1, 2s after failure 2, 4s after failure 3. This gives transient issues (brief network blips, momentary database timeouts) time to resolve before consuming the retry budget.

⏱️ Recommended Retry Schedule (Exponential Backoff)

Attempt 1
Immediate
→ fail →
Attempt 2
Wait 2s
→ fail →
Attempt 3
Wait 4s
→ fail →
Attempt 4
Wait 8s
→ fail →
Attempt 5
Wait 16s
→ fail →
💀 DLQ

🗺️ Section 11: Everything Together — Complete DLQ Architecture

💀 Dead Letter Queue — Complete Architecture Map

── PRODUCER ──
🖥️ Order / Payment / Email Service
→ publishes messages to Main Queue
⬇️
📬 Main Queue + Consumer (Normal Flow)
📦 Message received → Consumer processes → SUCCESS → DELETE (acknowledge)
💥 FAIL → visibility timeout expires → redelivered → retry (x maxReceiveCount)
🔄 Exponential backoff between retries (1s → 2s → 4s → DLQ)
⬇️ After maxReceiveCount failures
💀 DEAD LETTER QUEUE (DLQ) — Messages Preserved Here
☠️ Corrupt JSON msg
☠️ Unhandled schema
☠️ DB outage msgs
Retention: 14 days max | Preserved with original body + metadata
⬇️ immediately
🔔 CloudWatch Alarm
depth > 0 → ALERT
📧 Slack / PagerDuty
On-call engineer notified
☁️ S3 Archive
Long-term retention
📊 Metrics Dashboard
Failure analytics
⬇️ After bug fix
⏪ DLQ Replay → Messages moved back to Main Queue → Process successfully! ✅

📐 Section 12: Core Design Principles DLQ Teaches Us

🚫 Never Throw Away Unprocessable Messages — Ever

A deleted failed message is a lost business transaction. A lost payment. A ghost order. DLQ exists to prevent this. Always route failed messages somewhere observable before giving up. The cost of storing them is negligible. The cost of losing them is not.

🔔 DLQ Depth = 0 Is Your SLA

Your DLQ should be empty at all times in a healthy system. Any nonzero depth is a production incident — not a warning, not a todo. Alert aggressively. Investigate immediately. The longer a message sits in the DLQ, the harder it is to replay (stale state, expired sessions, time-sensitive operations).

🔁 Always Fix the Bug Before Replaying

Replaying messages into a broken consumer just re-populates the DLQ. Ensure the root cause is fully resolved and deployed before triggering replay. Replay gradually. Monitor for re-failures in real time during the replay.

♻️ Design Consumers to Be Idempotent Before Adding DLQ

DLQ replay only works safely if your consumer is idempotent. If replaying a "charge customer" message charges them again — DLQ replay causes more damage than the original failure. Idempotency and DLQ are a team — they must be designed together.

📊 Analyse DLQ Messages — They Are a Gift

DLQ messages tell you exactly what your system can't handle. They are free integration tests written by production data. Regularly analyse patterns in your DLQ messages: are the same error types recurring? The same producer? DLQ data can reveal schema mismatches before they become incidents.


🎓 Section 13: System Design Interview Cheat Sheet

DLQ questions appear frequently in system design interviews. Here is exactly how to answer every variant:

❓ "What is a Dead Letter Queue and why do we need it?"

Answer: A DLQ is a queue that holds messages that failed to be processed successfully after a configurable number of retries. We need it because without it, a single "poison pill" message can block the entire queue forever in an infinite retry loop, preventing all subsequent messages from being processed. The DLQ isolates failed messages, unblocks the main queue, and preserves the message for analysis and replay after the root cause is fixed.

❓ "How would you implement DLQ in Kafka?"

Answer: Kafka doesn't have a built-in DLQ. The pattern: wrap consumer processing in try/catch. On failure: produce the message to a Dead Letter Topic (e.g., orders.DLT) with metadata (original topic, error, timestamp). Then commit the main topic offset. A separate DLT consumer handles alerting, archiving to S3, and metrics. For retry patterns: use multiple retry topics with exponential delays before the DLT.

❓ "How do you safely replay messages from a DLQ?"

Answer: (1) Verify and deploy the bug fix first. (2) Inspect DLQ messages — are they still relevant? (e.g. expired OTPs should be discarded, not replayed). (3) Ensure consumer is idempotent. (4) Replay gradually in batches. (5) Monitor consumer error rate during replay — stop if errors spike again. In AWS SQS use StartMessageMoveTask. In Kafka, reset consumer group offset on DLT.

❓ "What should maxReceiveCount be set to?"

Answer: It depends on your use case. For payment systems: 3–5 retries (fail fast, alert early, prevent duplicate charges). For email delivery: 5–10 retries (transient SMTP issues resolve with time). For event analytics: up to 10–15 (lower criticality, higher tolerance for transient failures). Always combine with exponential backoff between retries.

❓ "How do you monitor a DLQ?"

Answer: Three critical metrics: (1) DLQ depth — alert immediately when > 0. (2) Age of oldest message — alert if message sits > 1 hour (risk of TTL expiry). (3) Rate of DLQ arrivals — a spike means a new failure mode, not just a single bad message. Use CloudWatch Alarms → SNS → PagerDuty/Slack. Also run a DLQ processor service that archives messages to S3 for post-mortem analysis.

❓ "Design a notification system — how does DLQ fit in?"

Answer: Notification service publishes to SQS (or Kafka). Email/SMS/Push services consume. If SMTP provider is down → email consumer fails → retried 3 times → DLQ. DLQ processor: archives to S3, alerts Slack, creates ticket. Once SMTP is restored: replay DLQ → emails delivered (might be delayed). DLQ prevents notification loss during provider outages and allows recovery replay.


🎉 Final Summary

💀 What is a DLQ: A queue that catches messages which failed processing after maxReceiveCount attempts, preserving them safely for analysis and replay
☠️ Poison Pill Problem: A broken message that crashes consumers on every attempt. Without DLQ, it blocks the entire queue forever in an infinite retry loop
🔄 Redrive Policy / maxReceiveCount: After N failures, the queue infrastructure automatically moves the message to DLQ. Pipeline unblocked. Message preserved.
☁️ SQS DLQ: Configure maxReceiveCount (3–5) + DLQ retention (14 days) + CloudWatch alarm on depth > 0 → every production queue must have this
📨 Kafka DLT: No native DLQ — implement in consumer: catch exceptions, produce to DLT topic with metadata, commit main offset. Separate DLT consumer alerts and archives
🐰 RabbitMQ DLX: Dead Letter Exchange routes rejected, expired, or overflowed messages to a dead letter queue. Three trigger conditions: reject, TTL, or queue overflow
🔔 DLQ Depth = 0: The only acceptable state in production. Alert immediately on any nonzero depth. Investigate every message. Never let DLQ grow silently
⏪ DLQ Replay: Fix bug first → verify idempotency → inspect message relevance → replay in batches → monitor for re-failures → confirm DLQ returns to zero
☁️ Archive to S3: Every DLQ message should be immediately archived to S3 for permanent retention — don't let the 14-day queue TTL be your only backup
🤝 DLQ + Idempotency = Team: DLQ enables replay. Idempotency makes replay safe. You need both. Neither works well without the other
✅ The Most Important Lesson from DLQ:

DLQ embodies a fundamental engineering philosophy: never silently discard failures — always make them visible and recoverable.

In distributed systems, failures are not edge cases. They are normal operating conditions. Networks fail. Services go down. Bad data happens. The question is not "will messages fail?" — they will. The question is "when they fail, do you know about it? Can you recover? Did you lose anything?"

A DLQ answers all three with a confident: Yes. Yes. No. That is the difference between a production-ready system and a liability. 🛡️


Happy Learning! Keep Building! 🔥

Comments