The Executors factory methods are dangerous because they hide an unbounded resource: newFixedThreadPool uses an unbounded queue (memory exhaustion) and newCachedThreadPool uses unbounded thread creation (thread exhaustion). Under sustained load both fail with OutOfMemoryError rather than degrading gracefully.
newFixedThreadPool — unbounded queue
public static ExecutorService newFixedThreadPool(int n) {
return new ThreadPoolExecutor(n, n,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()); // capacity = Integer.MAX_VALUE
}
The LinkedBlockingQueue is constructed with no bound. If tasks arrive faster than n threads can drain them, the queue grows without limit. There is no back-pressure, no rejection — just heap filling with queued Runnables until the JVM dies. A slow downstream dependency turns a traffic spike into an OOM heap dump.
newCachedThreadPool — unbounded threads
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
A SynchronousQueue has zero capacity, so every task must hand directly to a thread; if none is free, a new thread spawns — up to Integer.MAX_VALUE. Burst 50,000 requests and you ask for 50,000 platform threads, hitting OutOfMemoryError: unable to create new native thread and starving the whole machine.
newSingleThreadExecutor / newScheduledThreadPool
The single-thread variant also uses an unbounded LinkedBlockingQueue. newScheduledThreadPool uses a DelayedWorkQueue that is likewise unbounded.
The fix
Construct ThreadPoolExecutor directly with a bounded queue, an explicit maximumPoolSize, a named ThreadFactory, and a deliberate RejectedExecutionHandler (often CallerRunsPolicy for natural back-pressure).