Setting a Global Timeout for Multi‑threaded API Calls in Java
The article explains how to enforce a global timeout for multiple concurrent API calls in Java, demonstrates the pitfalls of applying per‑task timeouts with Future#get, and presents correct solutions using ThreadPoolExecutor.invokeAll and CompletableFuture.allOf with code examples.
When a program needs to call several downstream APIs in parallel, it is often required to guarantee that the whole batch finishes within a predefined total timeout. The example scenario uses three calls that take 10 s, 15 s and 20 s respectively, while the desired overall timeout is 15 s.
Wrong approach : each task is submitted to a ThreadPoolExecutor and Future#get(15, TimeUnit.SECONDS) is called on every future. Because the timeout is applied per task, the total execution time can exceed the intended limit.
<span>package com.renzhikeji.demo;</span>
<span>import java.util.ArrayList;</span>
<span>import java.util.List;</span>
<span>import java.util.concurrent.*;</span>
<span>public class JdkDemo {</span>
<span> private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10, 100, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), Thread::new, new ThreadPoolExecutor.AbortPolicy() {</span>
<span> @Override</span>
<span> public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {</span>
<span> System.out.println("rejectedExecution");</span>
<span> super.rejectedExecution(r, e);</span>
<span> }</span>
<span> });</span>
<span> public static void main(String[] args) {</span>
<span> List<Future<Integer>> futures = new ArrayList<>(10);</span>
<span> Future<Integer> future1 = poolExecutor.submit(() -> {</span>
<span> System.out.println(Thread.currentThread());</span>
<span> TimeUnit.SECONDS.sleep(10);</span>
<span> return 1;</span>
<span> });</span>
<span> futures.add(future1);</span>
<span> Future<Integer> future2 = poolExecutor.submit(() -> {</span>
<span> System.out.println(Thread.currentThread());</span>
<span> TimeUnit.SECONDS.sleep(15);</span>
<span> return 1;</span>
<span> });</span>
<span> futures.add(future2);</span>
<span> Future<Integer> future3 = poolExecutor.submit(() -> {</span>
<span> System.out.println(Thread.currentThread());</span>
<span> TimeUnit.SECONDS.sleep(20);</span>
<span> return 1;</span>
<span> });</span>
<span> futures.add(future3);</span>
<span> long start = System.currentTimeMillis();</span>
<span> for (Future<Integer> integerFuture : futures) {</span>
<span> try {</span>
<span> integerFuture.get(15, TimeUnit.SECONDS);</span>
<span> } catch (Throwable e) {</span>
<span> e.printStackTrace();</span>
<span> }</span>
<span> }</span>
<span> long d = System.currentTimeMillis() - start;</span>
<span> System.out.println(d / 1000);</span>
<span> }</span>
<span>}</span>The JavaDoc note clarifies that Future#get(long, TimeUnit) sets a timeout for each individual task, not for the whole batch.
Correct approach 1 – invokeAll : use ThreadPoolExecutor.invokeAll with a global timeout. The executor internally computes the remaining time for each task based on a common deadline, ensuring the total execution respects the specified limit.
<span>package com.renzhikeji.demo;</span>
<span>import java.util.ArrayList;</span>
<span>import java.util.Arrays;</span>
<span>import java.util.List;</span>
<span>import java.util.concurrent.*;</span>
<span>public class JdkDemo {</span>
<span> private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10, 100, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), Thread::new, new ThreadPoolExecutor.AbortPolicy() {</span>
<span> @Override</span>
<span> public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {</span>
<span> System.out.println("rejectedExecution");</span>
<span> super.rejectedExecution(r, e);</span>
<span> }</span>
<span> });</span>
<span> public static void main(String[] args) {</span>
<span> Callable<Integer> callable1 = () -> {</span>
<span> System.out.println(Thread.currentThread());</span>
<span> TimeUnit.SECONDS.sleep(10);</span>
<span> return 1;</span>
<span> };</span>
<span> Callable<Integer> callable2 = () -> {</span>
<span> System.out.println(Thread.currentThread());</span>
<span> TimeUnit.SECONDS.sleep(15);</span>
<span> return 1;</span>
<span> };</span>
<span> Callable<Integer> callable3 = () -> {</span>
<span> System.out.println(Thread.currentThread());</span>
<span> TimeUnit.SECONDS.sleep(20);</span>
<span> return 1;</span>
<span> };</span>
<span> long start = System.currentTimeMillis();</span>
<span> try {</span>
<span> List<Future<Integer>> invoked = poolExecutor.invokeAll(Arrays.asList(callable1, callable2, callable3), 15L, TimeUnit.SECONDS);</span>
<span> for (Future<Integer> future : invoked) {</span>
<span> try {</span>
<span> Integer a = future.get();</span>
<span> } catch (Throwable e) {</span>
<span> e.printStackTrace();</span>
<span> }</span>
<span> }</span>
<span> } catch (Throwable e) {</span>
<span> System.out.println("12");</span>
<span> e.printStackTrace();</span>
<span> }</span>
<span> long d = System.currentTimeMillis() - start;</span>
<span> System.out.println(d / 1000);</span>
<span> }</span>
<span>}</span>The executor calculates each task's remaining time as deadline – current time , which is why the overall execution stops after the global timeout.
Correct approach 2 – CompletableFuture : combine the three asynchronous tasks with CompletableFuture.allOf and apply a single timeout to the combined future.
<span>package com.renzhikeji.demo;</span>
<span>import java.util.concurrent.*;</span>
<span>import java.util.function.Supplier;</span>
<span>public class JdkDemo {</span>
<span> private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10, 100, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), Thread::new, new ThreadPoolExecutor.AbortPolicy() {</span>
<span> @Override</span>
<span> public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {</span>
<span> System.out.println("rejectedExecution");</span>
<span> super.rejectedExecution(r, e);</span>
<span> }</span>
<span> });</span>
<span> public static void main(String[] args) {</span>
<span> Supplier<Integer> callable1 = () -> {</span>
<span> System.out.println(Thread.currentThread());</span>
<span> try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }</span>
<span> return 1;</span>
<span> };</span>
<span> CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(callable1, poolExecutor);</span>
<span> Supplier<Integer> callable2 = () -> {</span>
<span> System.out.println(Thread.currentThread());</span>
<span> try { TimeUnit.SECONDS.sleep(15); } catch (InterruptedException e) { e.printStackTrace(); }</span>
<span> return 1;</span>
<span> };</span>
<span> CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(callable2, poolExecutor);</span>
<span> Supplier<Integer> callable3 = () -> {</span>
<span> System.out.println(Thread.currentThread());</span>
<span> try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }</span>
<span> return 1;</span>
<span> };</span>
<span> CompletableFuture<Integer> future3 = CompletableFuture.supplyAsync(callable3, poolExecutor);</span>
<span> long start = System.currentTimeMillis();</span>
<span> try {</span>
<span> CompletableFuture.allOf(future2, future2, future3).get(15L, TimeUnit.SECONDS);</span>
<span> } catch (Throwable e) { e.printStackTrace(); }</span>
<span> long d = System.currentTimeMillis() - start;</span>
<span> System.out.println(d / 1000);</span>
<span> try { Integer i = future1.get(); System.out.println("future1"); } catch (Throwable e) { e.printStackTrace(); }</span>
<span> try { Integer i = future2.get(); System.out.println("future2"); } catch (Throwable e) { e.printStackTrace(); }</span>
<span> try { Integer i = future3.get(); System.out.println("future3"); } catch (Throwable e) { e.printStackTrace(); }</span>
<span> }</span>
<span>}</span>Running this version shows that tasks 1 and 2 complete while task 3 is cancelled after the 15‑second global timeout.
Note: the thread pool’s core size must be at least the number of concurrent tasks; otherwise tasks wait in the queue and the effective timeout becomes longer.
Cognitive Technology Team
Cognitive Technology Team regularly delivers the latest IT news, original content, programming tutorials and experience sharing, with daily perks awaiting you.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.