skip to content

How does the concurrency setting work on a DefaultMessageListenerContainer, and what should you consider when tuning it (including for topics)?

level: middleimportance: should knowfreq 38%

answer

  1. "lower-upper" -> concurrentConsumers / maxConcurrentConsumers
  2. each consumer = thread + session
  3. scaling tuned by maxMessagesPerTask, idle limits
  4. parallel consumers break queue ordering
  5. topic non-shared: concurrency 1 or duplicates

basics

~20 s

On a DMLC, concurrency="lower-upper" (e.g. "3-10") sets how many consumer threads run: it starts at the lower number and scales up to the upper under load, then shrinks. More consumers means more parallel processing but also more sessions and possible message-ordering loss.

solid answer

~40 s

For DefaultMessageListenerContainer, setConcurrency("lower-upper") maps to setConcurrentConsumers(lower) and setMaxConcurrentConsumers(upper); a single value fixes the count. Each consumer is a thread with its own JMS session polling the destination, so more consumers give more parallelism and throughput. DMLC scales up toward the max when load is sustained (governed by maxMessagesPerTask, idleConsumerLimit, idleTaskExecutionLimit) and scales back when idle. Tuning considerations: throughput vs broker/connection/session resource limits; per-consumer thread and DB connection usage downstream; loss of ordering once you have parallel consumers on a queue; and for TOPICS, extra concurrent consumers each receive every message (duplicated processing) unless it's a shared subscription, so you normally keep topic concurrency at 1 (or use durable shared subscriptions). Also align with a suitably sized task executor and downstream pool.

code

java · 15 lines
java
@Bean
public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(ConnectionFactory cf) {
    var factory = new DefaultJmsListenerContainerFactory();
    factory.setConnectionFactory(cf);
    factory.setConcurrency("3-10");        // baseline 3, scale to 10 under load
    factory.setMaxMessagesPerTask(50);     // let the scheduler rebalance threads
    return factory;
}

// Per-endpoint override; keep topics at 1 unless using a shared subscription
@JmsListener(destination = "orders", concurrency = "5-20")
public void onOrder(Order o) { /* queue: parallel OK, ordering not guaranteed */ }

@JmsListener(destination = "priceUpdates", concurrency = "1")   // topic: avoid duplicate processing
public void onPrice(Price p) { }

go deeper

for a junior

Know concurrency="3-10" runs multiple consumer threads for more throughput.

for a middle

Explain concurrentConsumers/maxConcurrentConsumers mapping and the ordering trade-off.

for a senior

Discuss scaling knobs (maxMessagesPerTask, idle limits), prefetch, and sizing against downstream pools.

for a principal

Design concurrency against SLAs, ordering guarantees, broker/topic semantics (shared subscriptions), and end-to-end resource budgets.

## What 'concurrency' means A **consumer** in a listener container is a **thread** that owns a JMS `Session`/`MessageConsumer` and delivers messages to your listener. **Concurrency** = how many such consumer threads run in parallel for one endpoint. On a **DefaultMessageListenerContainer (DMLC)**: - `setConcurrentConsumers(int)` — the baseline number of consumers. - `setMaxConcurrentConsumers(int)` — the ceiling for dynamic scaling. - `setConcurrency(String)` — convenience: `"3-10"` → concurrentConsumers=3, maxConcurrentConsumers=10; a single `"5"` → fixed 5. You can set these on the container, on `DefaultJmsListenerContainerFactory`, per-endpoint via `@JmsListener(concurrency = "3-10")`, or globally in Boot via `spring.jms.listener.concurrency` / `max-concurrency`. ## How dynamic scaling works DMLC starts `concurrentConsumers` threads. Under **sustained load** it adds consumers toward `maxConcurrentConsumers`; when consumers sit **idle**, it scales back. The behavior is shaped by: - **`maxMessagesPerTask`** — how many messages a scheduled task processes before yielding; enables the scheduler to reclaim/rebalance threads (must be positive for scaling to work well). - **`idleConsumerLimit`** — max number of idle consumers kept before shrinking. - **`idleTaskExecutionLimit`** — how many idle receive attempts before a consumer is a candidate to stop. - **`receiveTimeout`** — poll timeout; interacts with how quickly idleness is detected. Scaling also requires the underlying **`TaskExecutor`** to have enough threads. ## Tuning considerations 1. **Throughput vs resources.** Each consumer = a thread + a JMS session + often a downstream DB connection. Setting concurrency to 50 while your DB pool is 10 just creates contention. Size consumers with the downstream bottleneck. 2. **Broker limits.** Brokers cap connections/sessions; too many consumers can exhaust them or hit prefetch issues. 3. **Ordering.** A single consumer on a queue preserves order; **multiple consumers process in parallel and destroy strict ordering.** If order matters, keep concurrency=1 or use message groups / partitioning where the broker supports it. 4. **Prefetch.** Broker prefetch interacts with concurrency: high prefetch with many consumers can cause uneven load distribution (one consumer hoards messages). 5. **Latency floor.** `receiveTimeout` polling adds a small latency; not usually significant. ## Topics are special For a **Topic** (pub/sub) with a **non-shared** subscription, **every consumer receives every message** — so running concurrency > 1 means the **same message is processed multiple times** (once per consumer). Therefore: - Keep topic listeners at **concurrency 1** by default (`pubSubDomain=true` containers). - To parallelize a topic, use a **shared (durable) subscription** (JMS 2.0 `createSharedConsumer`/`createSharedDurableConsumer`) so consumers split the stream instead of duplicating it; configure via `setSubscriptionShared(true)` / subscription name. ## Gotchas - Putting a range on an **SMLC** does nothing (fixed count) — scaling is DMLC-only. - Raising concurrency without raising the `TaskExecutor`/DB pool sizes yields no gain. - Assuming ordering survives multiple consumers. - Cranking topic concurrency and getting duplicate processing.

  • Why keep concurrency at 1 for a topic listener?
    With a non-shared topic subscription each consumer receives every message, so concurrency>1 processes the same message multiple times. Use a shared/durable subscription (JMS 2.0) if you need to parallelize a topic.
  • You bumped concurrency from 5 to 30 but throughput didn't improve. What would you check?
    The real bottleneck: downstream DB connection pool size, broker session/connection limits and prefetch, and the container's TaskExecutor thread count. Extra consumers can't help if a shared resource downstream is saturated.

saying these in an interview costs you the question

  • Thinking more consumers always preserve message ordering.
  • Setting high concurrency on a non-shared topic and not realizing messages are processed multiple times.
  • Expecting a concurrency range to scale on SimpleMessageListenerContainer.

context