Home Java Spring Cloud AWS Messaging: SQS and SNS Integration
Advanced 7 min · July 14, 2026

Spring Cloud AWS Messaging: SQS and SNS Integration

Master Spring Cloud AWS messaging with SQS and SNS.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 20-25 min read
  • Java 17+
  • Spring Boot 3.x
  • Basic understanding of AWS SQS and SNS
  • AWS account (or LocalStack for local development)
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Use @SqsListener for polling SQS queues with built-in backoff and error handling.
  • Prefer SNS for fan-out notifications; SQS for reliable point-to-point messaging.
  • Configure visibility timeout and dead-letter queues to handle failures gracefully.
  • Avoid blocking calls inside listeners; use async processing for throughput.
  • Test locally with ElasticMQ or LocalStack to simulate AWS messaging.
✦ Definition~90s read
What is Spring Cloud AWS Messaging?

Spring Cloud AWS Messaging is a framework that integrates Amazon SQS and SNS into Spring Boot applications, providing annotations and templates for reliable message production and consumption.

Think of SNS like a radio station: it broadcasts a song (message) to anyone tuned in (subscribers).
Plain-English First

Think of SNS like a radio station: it broadcasts a song (message) to anyone tuned in (subscribers). SQS is like a mailbox: each message sits in a box until someone picks it up. You can have the radio station drop a note in the mailbox (SNS sends to SQS) so you never miss a message even if you step away.

Messaging is the backbone of any resilient microservices architecture. If your services talk to each other via synchronous HTTP calls, you're building a house of cards. One service goes down, and the whole chain collapses. That's where Spring Cloud AWS messaging comes in, giving you battle-tested integration with Amazon SQS and SNS.

I've been building microservices since the Netflix stack days, and I've seen teams butcher messaging patterns more times than I care to admit. They treat SQS like a database, poll every second, and wonder why their bill is astronomical. Or they use SNS without idempotency and duplicate payments all over the place.

In this article, I'll show you how to integrate SQS and SNS the right way using Spring Cloud AWS. We'll cover configuration, production patterns, and the gotchas that the official docs gloss over. By the end, you'll know how to build messaging that scales, handles failures, and doesn't bankrupt you on AWS costs.

We'll use realistic examples from payment processing and event-driven architectures. No more generic User/Order nonsense. You'll see how to process payment events reliably, handle retries, and debug issues when things go sideways at 2 AM.

Setting Up Spring Cloud AWS Messaging

First things first: add the right dependencies. Spring Cloud AWS has evolved. If you're still using the old spring-cloud-aws-messaging from the 2.x line, you're missing out. The 3.x line (Spring Cloud AWS 3.0+) is a complete rewrite backed by the AWS SDK v2. It's faster, supports reactive, and has sane defaults.

```xml <dependencyManagement> <dependencies> <dependency> <groupId>io.awspring.cloud</groupId> <artifactId>spring-cloud-aws-dependencies</artifactId> <version>3.1.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>

<dependency> <groupId>io.awspring.cloud</groupId> <artifactId>spring-cloud-aws-starter-sqs</artifactId> </dependency> <dependency> <groupId>io.awspring.cloud</groupId> <artifactId>spring-cloud-aws-starter-sns</artifactId> </dependency> ```

Now configure your AWS credentials. I've seen teams hardcode them in properties files and then commit to GitHub. Don't be that person. Use IAM roles if on EC2/EKS, or environment variables locally.

``properties # application.yml spring: cloud: aws: region: static: us-east-1 credentials: access-key: ${AWS_ACCESS_KEY_ID} secret-key: ${AWS_SECRET_ACCESS_KEY} sqs: listener: poll-timeout: 20s max-concurrent-messages: 10 ``

The poll-timeout enables long polling, which reduces empty receives and saves money. max-concurrent-messages controls how many messages your listener processes in parallel. Tune this based on your downstream dependencies.

pom.xmlJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>io.awspring.cloud</groupId>
      <artifactId>spring-cloud-aws-dependencies</artifactId>
      <version>3.1.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependency>
  <groupId>io.awspring.cloud</groupId>
  <artifactId>spring-cloud-aws-starter-sqs</artifactId>
</dependency>
<dependency>
  <groupId>io.awspring.cloud</groupId>
  <artifactId>spring-cloud-aws-starter-sns</artifactId>
</dependency>
⚠ Version Compatibility
📊 Production Insight
I once saw a team using the old spring-cloud-aws-messaging with SDK v1 and their SQS polling was causing 1000+ empty receives per minute. Switching to 3.x with long polling dropped their costs by 80%.
🎯 Key Takeaway
Use Spring Cloud AWS 3.x with AWS SDK v2 for better performance and reactive support.

Consuming SQS Messages with @SqsListener

The @SqsListener annotation is your bread and butter. It automatically polls the queue, deserializes the message body (JSON by default), and invokes your method. But here's where most people screw up: they treat the listener method like a regular controller method.

```java @Component public class PaymentEventListener {

private static final Logger log = LoggerFactory.getLogger(PaymentEventListener.class); private final PaymentService paymentService;

public PaymentEventListener(PaymentService paymentService) { this.paymentService = paymentService; }

@SqsListener("payment-events") public void handlePaymentEvent(PaymentEvent event) { log.info("Processing payment event: {}", event.paymentId()); paymentService.processPayment(event); } } ```

This looks innocent, but it's blocking. If processPayment takes 10 seconds, your listener thread is tied up. With max-concurrent-messages=10, you can handle 10 messages concurrently, but if each takes 10s, throughput is 1 msg/sec. Not great.

``java @SqsListener("payment-events") public void handlePaymentEvent(PaymentEvent event) { CompletableFuture.runAsync(() -> { try { paymentService.processPayment(event); } catch (Exception e) { log.error("Failed to process payment {}", event.paymentId(), e); // Let the framework handle retries throw e; } }); } ``

But be careful: if you throw an exception inside the async task, the framework won't know about it. The message will be deleted from the queue (since the listener returned successfully). You need to handle errors explicitly.

A better approach: use the @Async annotation on the service method and configure a thread pool. Or use reactive programming with Spring WebFlux.

What the docs don't tell you: the default error handler deletes messages on success and redrives on exception. If you catch the exception and don't rethrow, the message is gone forever. Always rethrow runtime exceptions for transient failures.

PaymentEventListener.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
public class PaymentEventListener {

    private static final Logger log = LoggerFactory.getLogger(PaymentEventListener.class);
    private final PaymentService paymentService;

    public PaymentEventListener(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    @SqsListener("payment-events")
    public void handlePaymentEvent(PaymentEvent event) {
        log.info("Processing payment event: {}", event.paymentId());
        try {
            paymentService.processPayment(event);
        } catch (NonRetryableException e) {
            log.error("Non-retryable error for payment {}", event.paymentId(), e);
            // Do NOT rethrow - message will be deleted
        } catch (Exception e) {
            log.error("Retryable error for payment {}", event.paymentId(), e);
            throw new RuntimeException(e); // Redrive to DLQ after maxReceiveCount
        }
    }
}
🔥Visibility Timeout
📊 Production Insight
A fintech startup using Spring Boot 3.1 on EKS had their payment listener timing out because visibility timeout was 30s and processing took 45s due to a downstream DB slowdown. Every message was processed twice, causing duplicate charges. They fixed it by increasing visibility timeout and adding a circuit breaker.
🎯 Key Takeaway
Make listeners async and handle exceptions carefully to avoid message loss or infinite redrives.

Publishing to SNS Topics

SNS is great for fan-out: one event, many subscribers. Spring Cloud AWS makes publishing trivial with SnsTemplate or NotificationMessagingTemplate. But here's the catch: SNS is fire-and-forget by default. If your subscriber is down, the message is lost. That's why the production pattern is SNS -> SQS -> Consumer.

```java @Service public class PaymentEventPublisher {

private final NotificationMessagingTemplate messagingTemplate;

public PaymentEventPublisher(NotificationMessagingTemplate messagingTemplate) { this.messagingTemplate = messagingTemplate; }

public void publishPaymentEvent(PaymentEvent event) { messagingTemplate.sendNotification("payment-events-topic", event, event.paymentId()); } } ```

The third parameter is the subject (optional). Under the hood, it serializes the object to JSON and publishes to the topic ARN (or name if you configured a prefix).

But what if the SNS topic doesn't exist? Spring Cloud AWS can auto-create topics if you enable it:

``properties spring.cloud.aws.sns.enable-sns-topic-creation=true ``

Don't rely on this in production. Create topics via CloudFormation or Terraform. Auto-creation can lead to unexpected costs and permissions issues.

Now, the critical part: idempotency. SNS does not guarantee exactly-once delivery to subscribers. If your subscriber is an SQS queue, the queue might receive duplicates. Always design your consumers to be idempotent.

One more thing: SNS message filtering. You can set filter policies on subscriptions so that only certain messages reach the queue. This reduces noise and saves costs. For example, only forward events with type=payment.

PaymentEventPublisher.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class PaymentEventPublisher {

    private final NotificationMessagingTemplate messagingTemplate;

    public PaymentEventPublisher(NotificationMessagingTemplate messagingTemplate) {
        this.messagingTemplate = messagingTemplate;
    }

    public void publishPaymentEvent(PaymentEvent event) {
        messagingTemplate.sendNotification("payment-events-topic", event, event.paymentId());
    }
}
💡Message Attributes
📊 Production Insight
I've seen teams publish directly to SNS and expect the consumer to handle retries. When the consumer was down for maintenance, they lost hours of events. Always buffer with SQS.
🎯 Key Takeaway
Use SNS for fan-out but always attach an SQS queue for reliable consumption.

What the Official Docs Won't Tell You

The official Spring Cloud AWS documentation is decent, but it glosses over several production realities. Here are the gotchas I've encountered:

1. FIFO Queue Limitations FIFO queues guarantee exactly-once processing, but they have a throughput limit of 3000 messages per second (with batching). If you need more, you're out of luck. Also, the consumer group concept doesn't exist in SQS. If you have multiple consumers on a FIFO queue, each message is processed by exactly one consumer, but ordering is per message group ID. Choose your group ID wisely: use customer ID or order ID to maintain ordering per entity.

2. Deserialization Failures By default, Spring Cloud AWS uses Jackson to deserialize JSON. If your message payload changes (e.g., new field added), deserialization might fail silently. The message will be redriven to DLQ. To handle schema evolution, use @JsonIgnoreProperties(ignoreUnknown = true) on your DTO or use a version field.

3. Long Polling vs Short Polling Short polling (default) returns immediately even if the queue is empty. This causes high AWS costs due to empty receives. Always enable long polling with poll-timeout=20s. But don't set it too high (max 20s) or your threads will be blocked.

4. The DLQ Configuration If you don't configure a dead-letter queue, messages that fail processing are retried indefinitely (up to maxReceiveCount, default 3). After that, they disappear. Always set a DLQ with a reasonable maxReceiveCount (3-5). Monitor the DLQ for anomalies.

5. SNS Subscription Confirmation When you subscribe an SQS queue to an SNS topic, AWS sends a confirmation message to the queue. Spring Cloud AWS auto-confirms subscriptions by default, but if the queue ARN is wrong, it fails silently. Always verify subscriptions in the AWS Console.

6. CloudWatch Metrics Don't rely on logs alone. Monitor ApproximateNumberOfMessagesVisible, NumberOfMessagesReceived, and NumberOfMessagesDeleted. A sudden drop in deleted messages indicates processing failures.

PaymentEvent.javaJAVA
1
2
3
4
5
6
7
8
@JsonIgnoreProperties(ignoreUnknown = true)
public record PaymentEvent(
    String paymentId,
    String customerId,
    BigDecimal amount,
    String currency,
    @JsonProperty("event_version") int eventVersion
) {}
⚠ FIFO Throughput
📊 Production Insight
A SaaS company using FIFO queues for billing events hit the throughput limit during Black Friday. Their payment pipeline stalled. They had to shard by customer ID across multiple FIFO queues. Always load test for peak traffic.
🎯 Key Takeaway
Know the hidden limitations of SQS/SNS: FIFO throughput, deserialization gotchas, and cost implications of short polling.

Error Handling and Retries with DLQ

Let's talk about the redrive policy. When your listener throws an exception, Spring Cloud AWS increments the receive count. After maxReceiveCount (default 3), the message is sent to the dead-letter queue (DLQ). But here's the nuance: if you catch the exception and don't rethrow, the message is considered successfully processed and deleted. That's bad if you want to retry.

```yaml PaymentQueue: Type: AWS::SQS::Queue Properties: QueueName: payment-events RedrivePolicy: deadLetterTargetArn: !GetAtt PaymentDLQ.Arn maxReceiveCount: 5

PaymentDLQ: Type: AWS::SQS::Queue Properties: QueueName: payment-events-dlq ```

Now, in your listener, decide which exceptions are retryable. For transient errors (network timeout, database deadlock), rethrow a RuntimeException. For permanent errors (invalid message format), catch and log, but don't rethrow so the message is deleted.

``java @SqsListener("payment-events") public void handle(PaymentEvent event) { try { paymentService.process(event); } catch (InvalidPaymentException e) { log.error("Invalid payment, discarding: {}", event.paymentId(), e); // No rethrow -> message deleted } catch (RetryableException e) { log.warn("Retryable error for payment {}", event.paymentId(), e); throw new RuntimeException(e); // Redrive } } ``

But what if you want to retry with exponential backoff? SQS doesn't support that natively. You can implement a custom retry by re-queuing the message with a delay. Or use a more sophisticated approach: send the message to a separate retry queue with a delay, and after max attempts, move to DLQ.

```java @SqsListener("payment-events") @Retryable(value = {RetryableException.class}, maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier = 2)) public void handle(PaymentEvent event) { paymentService.process(event); }

@Recover public void recover(RetryableException e, PaymentEvent event) { log.error("Payment failed after retries: {}", event.paymentId(), e); // Send to DLQ manually or just let it go } ```

This works, but note: the @Recover method is called after all retries fail, and the message is still considered processed (deleted) because the method returned without exception. So you lose the message unless you manually send it to DLQ.

PaymentEventListener.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
@SqsListener("payment-events")
@Retryable(value = {RetryableException.class}, maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier = 2))
public void handle(PaymentEvent event) {
    paymentService.process(event);
}

@Recover
public void recover(RetryableException e, PaymentEvent event) {
    log.error("Payment failed after retries: {}", event.paymentId(), e);
    // Optionally send to DLQ manually
    sqsTemplate.send("payment-events-dlq", event);
}
🔥DLQ Monitoring
📊 Production Insight
An e-commerce platform had a bug where all payment messages ended up in DLQ because of a JSON parsing error. They didn't monitor DLQ, and thousands of orders were never fulfilled. Set up alerts!
🎯 Key Takeaway
Use DLQ with appropriate maxReceiveCount. Differentiate between retryable and non-retryable exceptions.

Testing SQS/SNS Integration Locally

You can't always hit real AWS queues during development. That's where local emulators come in. ElasticMQ is a lightweight SQS emulator. LocalStack provides both SQS and SNS.

For ElasticMQ, run it with Docker:

``bash docker run -p 9324:9324 -p 9325:9325 softwaremill/elasticmq-native ``

``properties spring.cloud.aws.sqs.endpoint=http://localhost:9324 spring.cloud.aws.sqs.region=us-east-1 spring.cloud.aws.credentials.access-key=test spring.cloud.aws.credentials.secret-key=test ``

``bash docker run -p 4566:4566 localstack/localstack ``

``properties spring.cloud.aws.sqs.endpoint=http://localhost:4566 spring.cloud.aws.sns.endpoint=http://localhost:4566 spring.cloud.aws.region.static=us-east-1 ``

Now you can create queues and topics programmatically or via AWS CLI pointing to localhost.

Here's a test example using @SpringBootTest and @TestContainers:

```java @SpringBootTest @Testcontainers class PaymentEventIntegrationTest {

@Container static LocalStackContainer localstack = new LocalStackContainer(DockerImageName.parse("localstack/localstack:3.0")) .withServices(SQS, SNS);

@DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.cloud.aws.sqs.endpoint", () -> localstack.getEndpointOverride(SQS).toString()); registry.add("spring.cloud.aws.sns.endpoint", () -> localstack.getEndpointOverride(SNS).toString()); registry.add("spring.cloud.aws.region.static", () -> localstack.getRegion()); }

@Autowired private SqsTemplate sqsTemplate;

@Autowired private NotificationMessagingTemplate snsTemplate;

@Test void testPublishAndConsume() { // Create queue and topic CreateQueueResult queue = sqsTemplate.createQueue("test-queue"); CreateTopicResult topic = snsTemplate.createTopic("test-topic"); // Subscribe snsTemplate.subscribe(topic.topicArn(), "sqs", queue.queueUrl()); // Publish snsTemplate.sendNotification(topic.topicArn(), new PaymentEvent("1", "cust1", BigDecimal.TEN, "USD", 1), "test"); // Poll List<Message> messages = sqsTemplate.receive(queue.queueUrl()); assertThat(messages).hasSize(1); } }

PaymentEventIntegrationTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@SpringBootTest
@Testcontainers
class PaymentEventIntegrationTest {

    @Container
    static LocalStackContainer localstack = new LocalStackContainer(DockerImageName.parse("localstack/localstack:3.0"))
            .withServices(SQS, SNS);

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.cloud.aws.sqs.endpoint", () -> localstack.getEndpointOverride(SQS).toString());
        registry.add("spring.cloud.aws.sns.endpoint", () -> localstack.getEndpointOverride(SNS).toString());
        registry.add("spring.cloud.aws.region.static", () -> localstack.getRegion());
    }

    @Autowired
    private SqsTemplate sqsTemplate;

    @Autowired
    private NotificationMessagingTemplate snsTemplate;

    @Test
    void testPublishAndConsume() {
        CreateQueueResult queue = sqsTemplate.createQueue("test-queue");
        CreateTopicResult topic = snsTemplate.createTopic("test-topic");
        snsTemplate.subscribe(topic.topicArn(), "sqs", queue.queueUrl());
        snsTemplate.sendNotification(topic.topicArn(), new PaymentEvent("1", "cust1", BigDecimal.TEN, "USD", 1), "test");
        List<Message> messages = sqsTemplate.receive(queue.queueUrl());
        assertThat(messages).hasSize(1);
    }
}
💡TestContainers
📊 Production Insight
I've seen teams write unit tests with mocks that pass but fail in staging because of real AWS permissions. Always test against emulators.
🎯 Key Takeaway
Use ElasticMQ or LocalStack for local development and TestContainers for integration tests.

Performance Tuning and Best Practices

Let's squeeze performance out of your messaging system. Here are the knobs you should tune:

1. Concurrency max-concurrent-messages controls how many messages your listener processes in parallel. Default is 10. If your processing is I/O-bound (calling external APIs), increase it to 20-50. But be careful: each thread consumes memory. Monitor heap usage.

2. Long Polling poll-timeout should be set to 20s (max). This reduces empty receives. But if you need lower latency, set it to 5-10s. Balance cost vs latency.

3. Batch Size When publishing, batch messages to SQS. Use SqsTemplate.sendBatch() to send up to 10 messages at once. This reduces API calls and costs.

4. Message Size SQS supports up to 256KB per message. If you need larger payloads, use S3 with a pointer in the message (S3 link). Or compress the payload.

5. Consumer Idempotency As stressed earlier, always implement idempotency. Use a database table to track processed message IDs. For high throughput, use Redis with TTL.

6. Monitoring Set up CloudWatch dashboards for queue depth, age of oldest message, and DLQ depth. Alert on anomalies.

``properties spring.cloud.aws.sqs.listener.poll-timeout=20s spring.cloud.aws.sqs.listener.max-concurrent-messages=20 spring.cloud.aws.sqs.listener.queue-name=payment-events spring.cloud.aws.sqs.listener.max-number-of-messages=10 ``

```java @Bean public SqsTemplate sqsTemplate(SqsAsyncClient sqsAsyncClient) { return SqsTemplate.builder().sqsAsyncClient(sqsAsyncClient).build(); }

public void publishBatch(List<PaymentEvent> events) { List<SqsBatchMessage> messages = events.stream() .map(event -> SqsBatchMessage.builder() .messageBody(event.toString()) .id(UUID.randomUUID().toString()) .build()) .toList(); sqsTemplate.sendBatch("payment-events", messages); } ```

BatchPublisher.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class BatchPublisher {

    private final SqsTemplate sqsTemplate;

    public BatchPublisher(SqsTemplate sqsTemplate) {
        this.sqsTemplate = sqsTemplate;
    }

    public void publishBatch(List<PaymentEvent> events) {
        List<SqsBatchMessage> messages = events.stream()
            .map(event -> SqsBatchMessage.builder()
                .messageBody(event.toString())
                .id(UUID.randomUUID().toString())
                .build())
            .toList();
        sqsTemplate.sendBatch("payment-events", messages);
    }
}
🔥Batch Errors
📊 Production Insight
A real-time analytics platform was processing 10k events/second. They tuned concurrency to 100 and used batch publishing. Their SQS costs dropped 40% and latency went from 5s to 500ms.
🎯 Key Takeaway
Tune concurrency, use long polling, batch publishes, and monitor everything.
● Production incidentPOST-MORTEMseverity: high

The Invisible Message Duplication Disaster

Symptom
Customers complained of being charged twice for the same order. Payment logs showed two identical charge requests arriving seconds apart.
Assumption
The developer assumed SQS deduplicates messages automatically (it does for FIFO, but they used standard queues).
Root cause
Standard SQS queues guarantee at-least-once delivery. Combined with a lack of idempotency in the payment processor, duplicate messages caused double charges.
Fix
Implemented idempotency keys in the payment service and switched to FIFO queues for payment events. Added a distributed lock (DynamoDB) to prevent concurrent processing of the same message.
Key lesson
  • Always assume SQS can deliver the same message twice (standard queues).
  • Idempotency is not optional for messaging consumers.
  • FIFO queues are more expensive but guarantee exactly-once delivery for critical flows.
  • Use message deduplication IDs for FIFO queues.
  • Test with duplicate message injection in staging.
Production debug guideSymptom to Action4 entries
Symptom · 01
Messages stuck in queue, never consumed
Fix
Check listener method signature and @SqsListener value. Verify queue name matches exactly. Look for exceptions in logs — unhandled exceptions cause redrive to DLQ after maxReceiveCount.
Symptom · 02
High latency in message processing
Fix
Check if listener is blocking (e.g., synchronous HTTP call). Increase concurrency with SimpleAsyncTaskExecutor or use @Async. Ensure visibility timeout is not too short causing redeliveries.
Symptom · 03
Duplicate messages despite FIFO queue
Fix
Verify message deduplication ID is unique per logical message. Check if producer retries with same dedup ID. Ensure consumer commits offset only after full processing.
Symptom · 04
SNS notifications not reaching SQS subscriber
Fix
Verify SQS queue policy allows SNS to send messages. Check SNS subscription confirmation (Auto-confirm with Spring Cloud AWS may fail if ARN mismatched). Use AWS Console to test publish.
★ Quick Debug Cheat SheetCommon symptoms and immediate actions for Spring Cloud AWS messaging issues.
Messages not consumed
Immediate action
Check queue ARN and region in application properties
Commands
aws sqs get-queue-attributes --queue-url <url> --attribute-names All
Check logs for 'No handler for SQS message' or 'AmazonSQSException'
Fix now
Ensure @SqsListener annotation has exact queue name
High AWS costs+
Immediate action
Check number of polling threads and long polling enabled
Commands
aws cloudwatch get-metric-statistics --namespace AWS/SQS --metric-name NumberOfEmptyReceives --statistics Sum
Review listener concurrency settings
Fix now
Enable long polling with spring.cloud.aws.sqs.listener.poll-timeout=20
Duplicate processing+
Immediate action
Check if queue is FIFO or standard
Commands
aws sqs get-queue-attributes --queue-url <url> --attribute-names FifoQueue
Verify message deduplication ID in logs
Fix now
Implement idempotency key in consumer
FeatureSQS StandardSQS FIFOSNS
DeliveryAt-least-onceExactly-onceAt-least-once
OrderingBest-effortGuaranteed per groupN/A
ThroughputUnlimited3000 msg/sUnlimited
Consumer patternPull (polling)Pull (polling)Push (HTTP/SQS/Lambda)
Use caseBackground jobsFinancial transactionsFan-out notifications
⚙ Quick Reference
7 commands from this guide
FileCommand / CodePurpose
pom.xmlSetting Up Spring Cloud AWS Messaging
PaymentEventListener.java@ComponentConsuming SQS Messages with @SqsListener
PaymentEventPublisher.java@ServicePublishing to SNS Topics
PaymentEvent.java@JsonIgnoreProperties(ignoreUnknown = true)What the Official Docs Won't Tell You
PaymentEventListener.java@SqsListener("payment-events")Error Handling and Retries with DLQ
PaymentEventIntegrationTest.java@SpringBootTestTesting SQS/SNS Integration Locally
BatchPublisher.java@ServicePerformance Tuning and Best Practices

Key takeaways

1
Use Spring Cloud AWS 3.x with AWS SDK v2 for modern messaging integration.
2
Always implement idempotency in consumers; SQS standard queues can deliver duplicates.
3
Configure DLQ with appropriate maxReceiveCount to handle failures gracefully.
4
Enable long polling and tune concurrency for cost-effective, high-throughput processing.
5
Test locally with ElasticMQ or LocalStack to avoid surprises in production.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the difference between SQS standard and FIFO queues. When would ...
Q02SENIOR
How does Spring Cloud AWS handle SQS message redrive to DLQ?
Q03SENIOR
Design a system to process payment events with SNS and SQS, ensuring exa...
Q01 of 03SENIOR

Explain the difference between SQS standard and FIFO queues. When would you use each?

ANSWER
Standard queues provide high throughput (virtually unlimited) but at-least-once delivery (duplicates possible). FIFO queues guarantee exactly-once processing and message ordering within a group, but max throughput is 3000 messages/second. Use standard for non-critical events where duplicates are acceptable; use FIFO for financial transactions or order processing where ordering and deduplication are critical.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is the difference between SQS and SNS?
02
How do I handle duplicate messages in SQS?
03
Can I use Spring Cloud AWS with LocalStack?
04
What is a dead-letter queue (DLQ) and why do I need one?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Cloud. Mark it forged?

7 min read · try the examples if you haven't

Previous
Batch Processing with Spring Cloud Data Flow: Task Launchers and Schedules
28 / 34 · Spring Cloud
Next
Spring Cloud AWS RDS: Database Configuration and Connection Management