Introduction

ThreadPoolExecutor throws RejectedExecutionException when it cannot accept new tasks -- either because all threads are busy and the work queue is full, or because the executor has been shut down. Under production load, this causes request failures that cascade through dependent services.

This error reveals a capacity planning issue: the thread pool is undersized for the incoming workload.

Symptoms

  • Application throws "java.util.concurrent.RejectedExecutionException" under peak load
  • Error rate spikes during traffic bursts but recovers during normal traffic
  • Thread pool metrics show all threads active and queue at maximum capacity

Common Causes

  • Work queue capacity is too small for the burst traffic pattern
  • Thread pool maximum size is too small for the workload
  • Tasks take longer than expected, causing queue to fill up

Step-by-Step Fix

  1. 1.Configure a properly sized thread pool: Size the pool based on task characteristics.
  2. 2.```java
  3. 3.import java.util.concurrent.*;

public class ThreadPoolConfig { public static ExecutorService createPool() { int corePoolSize = Runtime.getRuntime().availableProcessors(); int maxPoolSize = corePoolSize * 2;

return new ThreadPoolExecutor( corePoolSize, maxPoolSize, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000), new ThreadPoolExecutor.CallerRunsPolicy() ); } } ```

  1. 1.Use CallerRunsPolicy to apply backpressure: Instead of rejecting, run the task in the calling thread.
  2. 2.```java
  3. 3.ThreadPoolExecutor executor = new ThreadPoolExecutor(
  4. 4.10, 20, 60, TimeUnit.SECONDS,
  5. 5.new ArrayBlockingQueue<>(500),
  6. 6.new ThreadPoolExecutor.CallerRunsPolicy()
  7. 7.);

// When queue is full, the calling thread executes the task itself. // This naturally slows down the producer, applying backpressure. ```

  1. 1.Implement custom rejection handler with metrics: Log and alert when tasks are rejected.
  2. 2.```java
  3. 3.executor.setRejectedExecutionHandler((r, exec) -> {
  4. 4.logger.error("Task rejected! Pool: threads={}, queue={}, active={}",
  5. 5.exec.getPoolSize(),
  6. 6.exec.getQueue().size(),
  7. 7.exec.getActiveCount());

metricsCounter("threadpool.rejected").increment();

try { Thread.sleep(100); exec.execute(r); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RejectedExecutionException("Retry interrupted", e); } }); ```

  1. 1.Monitor thread pool health: Expose thread pool metrics for monitoring.
  2. 2.```java
  3. 3.public class ThreadPoolMonitor {
  4. 4.private final ThreadPoolExecutor pool;

public ThreadPoolMonitor(ThreadPoolExecutor pool) { this.pool = pool; ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor(); monitor.scheduleAtFixedRate(this::report, 0, 30, TimeUnit.SECONDS); }

private void report() { logger.info("Pool: size={}, active={}, queue={}, completed={}", pool.getPoolSize(), pool.getActiveCount(), pool.getQueue().size(), pool.getCompletedTaskCount()); } } ```

Prevention

  • Size thread pools based on measured task duration, not guesses
  • Use CallerRunsPolicy to apply natural backpressure under load
  • Monitor thread pool metrics (queue depth, active threads, rejection count)
  • Implement circuit breakers to shed load before the thread pool saturates