skip to content

What does throttleLimit control in a multithreaded step, and how has its role changed in Spring Batch 5?

level: middleimportance: should knowfreq 35%

answer

  1. in-flight chunk cap / semaphore on submission
  2. default was 4 -> classic under-parallelism bug
  3. deprecated in Spring Batch 5
  4. size ThreadPoolTaskExecutor instead
  5. effective = min(throttle, pool)

basics

~20 s

throttleLimit caps how many chunks can be processed concurrently (in flight at once), regardless of pool size. In Spring Batch 5 the throttleLimit(...) builder method is deprecated; you instead size the TaskExecutor's thread pool directly.

solid answer

~40 s

In a multithreaded step, the step submits chunk iterations to a TaskExecutor. `throttleLimit` bounds the number of concurrent chunk tasks the step will keep in flight at any moment — it throttles submission so the step doesn't queue work faster than workers drain it. Historically it defaulted to 4, which was a common gotcha: even with a 20-thread pool, only 4 chunks ran at once until you raised the throttle. In Spring Batch 5, `StepBuilder.throttleLimit(int)` is **deprecated**; the framework now expects you to express concurrency through the executor itself — core/max pool size and queue capacity on `ThreadPoolTaskExecutor`. So the practical answer: throttleLimit historically had to be raised to match your pool; today, tune the pool and treat throttleLimit as legacy.

code

java · 15 lines
java
// Spring Batch 4 style (throttleLimit still needed):
new StepBuilder("step", jobRepository)
    .<In, Out>chunk(50, tx)
    .reader(reader).writer(writer)
    .taskExecutor(pool)
    .throttleLimit(16)   // must raise from default 4 to use a 16-thread pool
    .build();

// Spring Batch 5 style (tune the executor, throttleLimit deprecated):
ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
pool.setCorePoolSize(16);
pool.setMaxPoolSize(16);
pool.setQueueCapacity(16);
pool.afterPropertiesSet();
// ...just .taskExecutor(pool), no throttleLimit call

go deeper

for a junior

Know throttleLimit limits how many chunks run at once.

for a middle

Explain the default-4 under-parallelism gotcha and that v5 deprecates it in favor of executor sizing.

for a senior

Reason about effective parallelism = min(throttle, pool) and aligning concurrency with downstream limits (DB pool, API rate).

for a principal

Advise migrating configuration to bounded ThreadPoolTaskExecutor with a rejection policy; design concurrency around the true bottleneck rather than a magic number.

## What throttleLimit is A multithreaded step drives its loop with a `TaskExecutorRepeatTemplate`. As it iterates, it submits each chunk-processing task to the configured `TaskExecutor`. Without a bound, an eager step could submit a huge number of tasks that pile up in the executor's queue. `throttleLimit` is the **maximum number of concurrent (in-flight) chunk tasks** the step template will allow before it blocks and waits for one to finish. Think of it as a semaphore around task submission. ## The classic default-4 gotcha In Spring Batch 4 and earlier, `throttleLimit` **defaulted to 4**. This produced a very common surprise: a developer configures a `ThreadPoolTaskExecutor` with, say, `corePoolSize=20`, expects 20 chunks in parallel, but only ever sees 4 threads busy. The reason is the step never submits more than `throttleLimit` chunks at a time, so the extra pool threads sit idle. The fix was to explicitly set `.throttleLimit(20)` to match (or align with) the pool size. ## Spring Batch 5 change As of **Spring Batch 5**, `StepBuilder.throttleLimit(int)` (and the underlying setter) is **deprecated**. The guidance is to control concurrency through the `TaskExecutor` configuration instead: - `corePoolSize` / `maxPoolSize` — how many worker threads exist. - `queueCapacity` — how much work can wait. By sizing the executor properly (and, if you want strict bounding, using a `ThreadPoolTaskExecutor` with a bounded queue and an appropriate rejection policy), you get the same throttling effect without the separate, easy-to-forget knob. Conceptually the responsibility moved from a Batch-specific parameter into standard Spring `TaskExecutor` tuning. ## Interaction with pool size Effective parallelism is roughly `min(throttleLimit, usable pool threads)`. If throttleLimit < pool size, the pool is under-utilized. If throttleLimit > pool size, tasks queue inside the executor. That's why aligning the two mattered — and why folding it into the executor in v5 removes the mismatch class of bugs. ## Practical guidance - On Batch 5: don't call `throttleLimit`; size `ThreadPoolTaskExecutor` (core=max=desired concurrency, bounded queue). - On older Batch: set `throttleLimit` explicitly, don't rely on the default of 4. - Match concurrency to the bottleneck (DB connection pool size, downstream API rate limits) — spinning up 50 threads against a 10-connection pool just creates contention.

  • A team sets corePoolSize=20 but sees only 4 threads busy on Spring Batch 4. Why?
    throttleLimit defaults to 4, so the step never keeps more than 4 chunks in flight regardless of pool size. They must set throttleLimit(20) (or migrate to v5 and size the executor).

saying these in an interview costs you the question

  • Claiming throttleLimit sets the thread pool size (it bounds in-flight chunks, not threads)
  • Assuming default throttleLimit equals the pool size
  • Not knowing it's deprecated in Spring Batch 5

context