Java
Concurrency
Runnable
Timeout
Multithreading

How can I implement a Runnable with timeout?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In Java, timeout behavior is usually implemented around a Runnable, not inside the Runnable interface itself. The standard pattern is to submit the task to an ExecutorService, wait through a Future, and cancel the task if it runs past the deadline.

Wrap the Runnable in an Executor Task

A Runnable has no built-in timeout API, so you need an execution framework that can track completion. ExecutorService provides that by returning a Future from submit.

java
1import java.util.concurrent.ExecutorService;
2import java.util.concurrent.Executors;
3import java.util.concurrent.Future;
4import java.util.concurrent.TimeUnit;
5import java.util.concurrent.TimeoutException;
6
7public class RunnableTimeoutDemo {
8    public static void main(String[] args) {
9        ExecutorService executor = Executors.newSingleThreadExecutor();
10
11        Runnable task = () -> {
12            try {
13                Thread.sleep(5000);
14                System.out.println("Task finished");
15            } catch (InterruptedException e) {
16                Thread.currentThread().interrupt();
17                System.out.println("Task interrupted");
18            }
19        };
20
21        Future<?> future = executor.submit(task);
22
23        try {
24            future.get(2, TimeUnit.SECONDS);
25            System.out.println("Completed within timeout");
26        } catch (TimeoutException e) {
27            future.cancel(true);
28            System.out.println("Timed out");
29        } catch (Exception e) {
30            e.printStackTrace();
31        } finally {
32            executor.shutdown();
33        }
34    }
35}

This is the usual production answer because it keeps the task logic separate from the timeout policy. The Runnable does the work. The caller decides how long it is allowed to run.

Understand What Cancellation Actually Means

future.cancel(true) does not forcibly kill code at an arbitrary instruction. It sends an interruption request to the worker thread. The task must cooperate by either reacting to InterruptedException or checking the interrupted flag.

java
1public class CooperativeTask implements Runnable {
2    @Override
3    public void run() {
4        while (!Thread.currentThread().isInterrupted()) {
5            doSmallUnitOfWork();
6        }
7        System.out.println("Stopping cleanly");
8    }
9
10    private void doSmallUnitOfWork() {
11        try {
12            Thread.sleep(100);
13        } catch (InterruptedException e) {
14            Thread.currentThread().interrupt();
15        }
16    }
17}

That cooperative model is important. If the task ignores interruption or blocks in an API that does not respond to interruption, the timeout may expire while the underlying work continues.

Use Callable When a Result Matters

If the caller needs a return value, switch to Callable instead of trying to mutate shared state from a Runnable. The timeout logic stays the same.

java
1import java.util.concurrent.Callable;
2import java.util.concurrent.ExecutionException;
3import java.util.concurrent.ExecutorService;
4import java.util.concurrent.Executors;
5import java.util.concurrent.Future;
6import java.util.concurrent.TimeUnit;
7import java.util.concurrent.TimeoutException;
8
9public class CallableTimeoutDemo {
10    public static void main(String[] args) {
11        ExecutorService executor = Executors.newSingleThreadExecutor();
12
13        Callable<String> task = () -> {
14            Thread.sleep(1000);
15            return "done";
16        };
17
18        Future<String> future = executor.submit(task);
19
20        try {
21            String result = future.get(2, TimeUnit.SECONDS);
22            System.out.println(result);
23        } catch (TimeoutException e) {
24            future.cancel(true);
25        } catch (InterruptedException | ExecutionException e) {
26            e.printStackTrace();
27        } finally {
28            executor.shutdown();
29        }
30    }
31}

This is often a cleaner design because it avoids extra shared variables and makes completion semantics explicit.

Consider a Scheduled Cancellation for Reuse

If many parts of the application need the same timeout behavior, a helper method can hide the boilerplate. One common variation is to submit the work and schedule a cancellation request separately.

java
1import java.util.concurrent.ExecutorService;
2import java.util.concurrent.Executors;
3import java.util.concurrent.Future;
4import java.util.concurrent.ScheduledExecutorService;
5import java.util.concurrent.TimeUnit;
6
7public class TimeoutHelper {
8    public static Future<?> runWithTimeout(Runnable task, long timeoutMillis) {
9        ExecutorService workerPool = Executors.newSingleThreadExecutor();
10        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
11
12        Future<?> future = workerPool.submit(task);
13        scheduler.schedule(() -> future.cancel(true), timeoutMillis, TimeUnit.MILLISECONDS);
14
15        workerPool.shutdown();
16        scheduler.shutdown();
17        return future;
18    }
19}

The same rule still applies: cancellation is only effective if the task cooperates.

Common Pitfalls

  • Assuming a timeout can force-stop arbitrary Java code immediately.
  • Ignoring interruption inside the task and then wondering why cancellation appears ineffective.
  • Using raw Thread management instead of the higher-level executor APIs.
  • Choosing Runnable when the caller really needs a computed result and should use Callable.
  • Forgetting to shut down the executor, which can leave non-daemon threads alive after the work is done.

Summary

  • Implement timeouts with ExecutorService and Future, not by extending Runnable.
  • Use future.get(timeout, unit) to wait for completion up to a deadline.
  • Call future.cancel(true) on timeout and make the task cooperate with interruption.
  • Prefer Callable when the task should return a value.
  • Treat timeout handling as cooperative cancellation, not forced termination.

Course illustration
Course illustration