What are the possible reason for a java.util.concurrent.RejectedExecutionException in a SingleThreadExecutor
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
A RejectedExecutionException from a single-thread executor means a submitted task was refused because the executor was not willing or able to accept more work. Even though Executors.newSingleThreadExecutor() uses one worker thread and a queue, it still follows standard executor lifecycle rules. In real applications, rejections usually come from shutdown state, lifecycle races, or custom one-thread executors with bounded queues.
What a Single-Thread Executor Guarantees
Executors.newSingleThreadExecutor() runs submitted tasks sequentially on one background thread.
It guarantees order of execution for accepted tasks, but it does not guarantee that submissions will always be accepted.
Most Common Cause: Submission After Shutdown
The most common cause is simple: code submits a task after shutdown() or shutdownNow() has already been called.
After shutdown begins, the executor may finish already-queued tasks, but it will reject new ones immediately.
Race Conditions Between Components
In larger systems, the issue is often not in one method. One component starts shutdown while another still believes the executor is open.
Typical sequence:
- A service begins shutdown.
- Another thread receives one last event.
- That thread submits background work.
- The executor rejects because shutdown already started.
This is a lifecycle coordination bug, not an executor implementation bug.
Mutable Executor Ownership Problems
Another cause is replacing executor instances while other code still holds old references.
If another component still submits to the old instance, it will see RejectedExecutionException after restart.
Custom One-Thread Executors Can Reject Without Shutdown
Not every one-thread executor is created with newSingleThreadExecutor(). Some systems use ThreadPoolExecutor with one worker and a bounded queue.
If tasks arrive faster than the worker can process them and the queue fills up, the executor rejects tasks even though it is not shutting down.
Check State, but Do Not Rely Only on It
You can reduce confusion by checking executor state before submitting:
This is useful for logging and fallback behavior, but it is not a complete fix because shutdown can still happen immediately after the check. Strong ownership rules are better than scattered state checks.
Add a Rejection Handler for Visibility
If you use a custom ThreadPoolExecutor, set a rejection handler so failures include more context.
This is much easier to debug than a bare exception with no surrounding lifecycle information.
Test Rejection Paths Explicitly
Do not test only the happy path. Rejection is often part of shutdown and resiliency behavior.
That kind of test is especially useful around shutdown hooks, schedulers, and long-lived service components.
Common Pitfalls
- Assuming a single-thread executor cannot reject because it has only one worker.
- Calling
shutdown()too early in the application lifecycle. - Letting several components own the same executor without a clear contract.
- Replacing executor instances while old references are still in use.
- Ignoring rejection behavior in custom bounded-queue executors.
Summary
- '
RejectedExecutionExceptionmeans the executor refused a new task.' - The usual cause is submission after shutdown.
- Lifecycle races are common when multiple components share one executor.
- Custom one-thread executors can also reject because of bounded queues.
- Clear ownership and explicit shutdown rules are the real solution.

