# Timeout Pattern

## Problem

When calling a remote service, if no timeout is set the caller can block indefinitely waiting for a response that may never come. Resources (threads, connections, memory) are tied up, latency balloons, and the failure propagates upstream.

## Solution

Set an upper bound on how long any operation is allowed to take. If the operation doesn't complete within the deadline, abort it and treat it as a failure. This frees resources quickly and allows the system to fail fast and apply other resilience patterns (retry, fallback).

## When to Use

- All remote service calls (HTTP, gRPC, database queries)
- File I/O and network operations
- Any blocking operation where indefinite waiting is unacceptable
- As a baseline resilience measure in every distributed system

## When to Avoid

- Long-running batch jobs where the duration is inherently variable (use heartbeats instead)
- User-initiated uploads/downloads where the user controls the pace
- Operations with their own internal progress tracking and cancellation

## Pseudocode

```
function with_timeout(operation, deadline):
    start timer(deadline)
    result = execute operation concurrently
    if timer fires first:
        cancel operation
        raise TimeoutError
    else:
        cancel timer
        return result
```

## Python Implementation

```python
import time
import threading
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeout


class OperationTimedOut(Exception):
    pass


def with_timeout(operation, timeout_seconds: float):
    with ThreadPoolExecutor(max_workers=1) as pool:
        future = pool.submit(operation)
        try:
            return future.result(timeout=timeout_seconds)
        except FuturesTimeout:
            raise OperationTimedOut(
                f"Operation timed out after {timeout_seconds}s"
            )


# --- Demo ---
def fast_operation():
    time.sleep(0.1)
    return "Fast result ready"


def slow_operation():
    time.sleep(5.0)
    return "This will never be seen"


print("=== Fast operation (timeout=2s) ===")
try:
    result = with_timeout(fast_operation, timeout_seconds=2.0)
    print(f"  Result: {result}")
except OperationTimedOut as e:
    print(f"  Timed out: {e}")

print()

print("=== Slow operation (timeout=0.5s) ===")
start = time.time()
try:
    result = with_timeout(slow_operation, timeout_seconds=0.5)
    print(f"  Result: {result}")
except OperationTimedOut as e:
    elapsed = time.time() - start
    print(f"  Timed out: {e}")
    print(f"  Returned control in {elapsed:.2f}s (not 5s)")

print()

print("=== Timeout with fallback ===")
def fetch_with_fallback():
    try:
        return with_timeout(slow_operation, timeout_seconds=0.3)
    except OperationTimedOut:
        return "Cached/default value (fallback)"

result = fetch_with_fallback()
print(f"  Result: {result}")
```

**Output:**
```
=== Fast operation (timeout=2s) ===
  Result: Fast result ready

=== Slow operation (timeout=0.5s) ===
  Timed out: Operation timed out after 0.5s
  Returned control in 0.50s (not 5s)

=== Timeout with fallback ===
  Result: Cached/default value (fallback)
```

## Go Implementation

```go
package main

import (
	"context"
	"fmt"
	"time"
)

func withTimeout[T any](d time.Duration, op func(ctx context.Context) (T, error)) (T, error) {
	ctx, cancel := context.WithTimeout(context.Background(), d)
	defer cancel()

	type result struct {
		val T
		err error
	}

	ch := make(chan result, 1)
	go func() {
		v, err := op(ctx)
		ch <- result{v, err}
	}()

	select {
	case <-ctx.Done():
		var zero T
		return zero, fmt.Errorf("operation timed out after %v", d)
	case r := <-ch:
		return r.val, r.err
	}
}

func main() {
	fast := func(ctx context.Context) (string, error) {
		time.Sleep(100 * time.Millisecond)
		return "Fast result ready", nil
	}

	slow := func(ctx context.Context) (string, error) {
		select {
		case <-time.After(5 * time.Second):
			return "This will never be seen", nil
		case <-ctx.Done():
			return "", ctx.Err()
		}
	}

	fmt.Println("=== Fast operation (timeout=2s) ===")
	res, err := withTimeout(2*time.Second, fast)
	if err != nil {
		fmt.Printf("  Timed out: %v\n", err)
	} else {
		fmt.Printf("  Result: %s\n", res)
	}

	fmt.Println()
	fmt.Println("=== Slow operation (timeout=500ms) ===")
	start := time.Now()
	res, err = withTimeout(500*time.Millisecond, slow)
	elapsed := time.Since(start)
	if err != nil {
		fmt.Printf("  Timed out: %v\n", err)
		fmt.Printf("  Returned control in %v (not 5s)\n", elapsed.Round(time.Millisecond))
	} else {
		fmt.Printf("  Result: %s\n", res)
	}

	fmt.Println()
	fmt.Println("=== Timeout with fallback ===")
	res, err = withTimeout(300*time.Millisecond, slow)
	if err != nil {
		res = "Cached/default value (fallback)"
	}
	fmt.Printf("  Result: %s\n", res)
}
```

## JavaScript Implementation

```javascript
class OperationTimedOut extends Error {
  constructor(timeoutMs) {
    super(`Operation timed out after ${timeoutMs}ms`);
  }
}

function withTimeout(operation, timeoutMs) {
  return Promise.race([
    operation(),
    new Promise((_, reject) =>
      setTimeout(() => reject(new OperationTimedOut(timeoutMs)), timeoutMs)
    ),
  ]);
}

// --- Demo ---
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function fastOperation() {
  await sleep(100);
  return "Fast result ready";
}

async function slowOperation() {
  await sleep(5000);
  return "This will never be seen";
}

(async () => {
  console.log("=== Fast operation (timeout=2000ms) ===");
  try {
    const result = await withTimeout(fastOperation, 2000);
    console.log(`  Result: ${result}`);
  } catch (e) {
    console.log(`  Timed out: ${e.message}`);
  }

  console.log();
  console.log("=== Slow operation (timeout=500ms) ===");
  const start = Date.now();
  try {
    const result = await withTimeout(slowOperation, 500);
    console.log(`  Result: ${result}`);
  } catch (e) {
    const elapsed = Date.now() - start;
    console.log(`  Timed out: ${e.message}`);
    console.log(`  Returned control in ${elapsed}ms (not 5000ms)`);
  }

  console.log();
  console.log("=== Timeout with fallback ===");
  let result;
  try {
    result = await withTimeout(slowOperation, 300);
  } catch {
    result = "Cached/default value (fallback)";
  }
  console.log(`  Result: ${result}`);
})();
```

## Related Patterns

- **Circuit Breaker** — repeated timeouts should trip the circuit breaker to stop sending requests to an unresponsive service.
- **Future/Promise** — timeouts on async operations are naturally expressed as a race between the future and a timer.
- **Fallback** — when a timeout occurs, a fallback provides a cached or default response rather than propagating the error.
