Backend Development 13 min read

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.

Cognitive Technology Team
Cognitive Technology Team
Cognitive Technology Team
Setting a Global Timeout for Multi‑threaded API Calls in Java

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.

backendJavaConcurrencythreadpoolCompletableFuturetimeout
Cognitive Technology Team
Written by

Cognitive Technology Team

Cognitive Technology Team regularly delivers the latest IT news, original content, programming tutorials and experience sharing, with daily perks awaiting you.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.