RejectedExecutionHandler in Java — Four Policies and Custom Implementations
Fast recall
Quick revision
Core answer
RejectedExecutionHandler is the interface a ThreadPoolExecutor calls when it cannot accept a new task — because the queue is full and all max threads are busy, or because the pool is shutting down. Four built-in policies: `AbortPolicy` (default) throws RejectedExecutionException; `CallerRunsPolicy` runs the task on the calling thread — natural backpressure; `DiscardPolicy` silently drops the task; `DiscardOldestPolicy` drops the oldest queued task and retries. For production services, `CallerRunsPolicy` is usually the right choice — it slows down producers instead of dropping work. Build a custom handler when you need to log rejections, emit metrics, apply backoff, redirect to a dead-letter queue, or implement priority-based shedding.Whether the candidate knows the four policies, when each fires, and how to pick the right one for a production workload
Using the default `AbortPolicy` silently and being surprised when production sees `RejectedExecutionException`. Using `DiscardPolicy` (silent drop) on critical work
Explain that `CallerRunsPolicy` creates natural backpressure by slowing the producer; mention metrics/dead-letter-queue patterns for custom handlers; note that `shutdownNow()` also triggers rejection for submits after shutdown
Interview-ready guide
Interview answer
RejectedExecutionHandler decides what happens when a ThreadPoolExecutor can't accept a task.
When rejection fires
- The queue is full AND all max threads are busy
- Or the pool has been shut down
- Only triggers if you have a bounded queue — unbounded queue means no rejection until OOM
The four built-in policies
`AbortPolicy` (default) — throws RejectedExecutionException
- Loud and explicit
- Often unhandled in practice — exceptions propagate from
submit()and crash requests - Good for tests; risky for production
`CallerRunsPolicy` — the production default
- Runs the rejected task on the submitting thread
- Creates natural backpressure — the submitter slows down, which propagates pressure upstream
- In an HTTP server, the request-accepting thread stops accepting new work until it finishes the rejected task
- Gotcha: can deadlock if the submitter is itself a pool thread
`DiscardPolicy` — silently drops the new task
- No log, no exception, no metric
- Only appropriate for idempotent fire-and-forget work like heartbeats or metrics where stale = useless
- Dangerous because it's silent; usually I wrap it with a custom handler that emits a metric
`DiscardOldestPolicy` — drops the oldest queued task, retries submission
- Appropriate when newer data is more valuable: market quotes, sensor readings, live UI state
Custom handlers are often the right answer
- Emit rejection metrics (Prometheus counter)
- Log at WARN with queue depth and active thread count
- Dead-letter queue for retry/replay
- Block-with-timeout: try to enqueue for N ms, then fail — middle ground between abort and caller-runs
- Priority shedding: drop low-priority first
- Kafka producer, Netflix Hystrix, Resilience4j all use custom rejection logic
The `RejectedExecutionHandler` interface is trivial
- Single method
rejectedExecution(Runnable, ThreadPoolExecutor) - Lambda-friendly; easy to wrap behavior around the built-in policies
The production checklist for every pool
- Bounded queue (otherwise rejection never fires)
- `CallerRunsPolicy` or a custom handler — not the default
AbortPolicy - Rejection metric emitted on every rejection
- Log at WARN when rejection happens
- Dead-letter queue if the work is important enough to retry
Recommendation: For a typical production service, use `new ThreadPoolExecutor(core, max, keepAlive, unit, new ArrayBlockingQueue<>(capacity), threadFactory, customCallerRunsWithMetrics)`. Start with CallerRunsPolicy for backpressure, then wrap it in a custom handler that emits a rejection counter and WARN log. Never ship a service with the default AbortPolicy plus an unbounded queue — that's a guaranteed production incident waiting to happen. If the work must not be lost, pair the pool with a dead-letter queue and a recovery thread that replays from it.
`RejectedExecutionHandler` in Java — Four Policies and Custom Implementations
Direct Answer
RejectedExecutionHandler is the interface a ThreadPoolExecutor calls when it cannot accept a new task — because the queue is full and all max threads are busy, or because the pool is shutting down. Four built-in policies: AbortPolicy (default) throws RejectedExecutionException; CallerRunsPolicy runs the task on the calling thread — natural backpressure; DiscardPolicy silently drops the task; DiscardOldestPolicy drops the oldest queued task and retries. For production services, CallerRunsPolicy is usually the right choice — it slows down producers instead of dropping work. Build a custom handler when you need to log rejections, emit metrics, apply backoff, redirect to a dead-letter queue, or implement priority-based shedding.
Common Mistakes & Pitfalls
Using the default AbortPolicy silently and being surprised when production sees RejectedExecutionException. Using DiscardPolicy (silent drop) on critical work
Check your understanding
Answer these without looking back at the article.
- How does `CallerRunsPolicy` create backpressure in an HTTP server?
- What happens to a task in the queue when `shutdownNow()` is called?
- Why is `DiscardPolicy` considered dangerous?
- How would you implement a 'block-with-timeout' rejection policy?
- Can `CallerRunsPolicy` deadlock, and when?
Related interview prep categories
2Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.