Skip to content
SP StackPractices
beginner

Patrón Timeout

Previene que las operaciones se cuelguen indefinidamente imponiendo un tiempo máximo de ejecución. Un patrón de resiliencia para tiempos de respuesta predecibles.

Temas: design

Patrón Timeout

Resumen

El Patrón Timeout es un patrón de resiliencia que previene que las operaciones se cuelguen indefinidamente imponiendo un tiempo máximo de ejecución. Sin timeouts, un único servicio descendiente lento puede retener hilos, conexiones y peticiones de usuarios indefinidamente, causando fallas en cascada a través del sistema.

Cuándo usarlo

Usa el Patrón Timeout cuando:

  • Llames a servicios externos, bases de datos o APIs que pueden volverse no responsivos
  • Necesites garantizar tiempos de respuesta máximos a usuarios o llamadores upstream
  • Las operaciones colgadas podrían agotar pools de hilos, conexiones o memoria
  • Quieras fallar rápidamente en lugar de esperar indefinidamente por una respuesta
  • Siempre combínalo con Retry para problemas transitorios, y Circuit Breaker para fallas crónicas

Solución

Python

import signal
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeout

def with_timeout(seconds: float):
    def decorator(func):
        def wrapper(*args, **kwargs):
            with ThreadPoolExecutor(max_workers=1) as executor:
                future = executor.submit(func, *args, **kwargs)
                try:
                    return future.result(timeout=seconds)
                except FutureTimeout:
                    raise TimeoutError(f"Operación timed out después de {seconds}s")
        return wrapper
    return decorator

@with_timeout(seconds=2.0)
def fetch_slow_data():
    import time
    time.sleep(5)
    return "data"

# Uso
try:
    result = fetch_slow_data()
    print(result)
except TimeoutError as e:
    print(f"Falló: {e}")

JavaScript

function withTimeout(fn, timeoutMs) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        reject(new Error(`Operación timed out después de ${timeoutMs}ms`));
      }, timeoutMs);

      Promise.resolve(fn(...args))
        .then(resolve)
        .catch(reject)
        .finally(() => clearTimeout(timer));
    });
  };
}

async function fetchSlowData() {
  await new Promise(r => setTimeout(r, 5000));
  return "data";
}

const timedFetch = withTimeout(fetchSlowData, 2000);

// Uso
timedFetch()
  .then(console.log)
  .catch(e => console.log("Falló:", e.message));

Java

import java.util.concurrent.*;

public class Timeout {
    public static <T> T execute(Callable<T> task, long timeoutMs) throws Exception {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        try {
            Future<T> future = executor.submit(task);
            return future.get(timeoutMs, TimeUnit.MILLISECONDS);
        } catch (TimeoutException e) {
            throw new RuntimeException("Operación timed out después de " + timeoutMs + "ms");
        } finally {
            executor.shutdownNow();
        }
    }

    public static void main(String[] args) {
        try {
            String result = execute(() -> {
                Thread.sleep(5000);
                return "data";
            }, 2000);
            System.out.println(result);
        } catch (Exception e) {
            System.out.println("Falló: " + e.getMessage());
        }
    }
}

Explicación

El Patrón Timeout impone un deadline duro en las operaciones:

  • Deadline: El tiempo máximo que una operación puede ejecutarse
  • Cancelación: Cuando el deadline expira, la operación se interrumpe o abandona
  • Propagación: Los timeouts deberían propagarse a través de la cadena de llamadas — si una llamada API tiene 5s, y llama a una DB que toma 4s, la llamada a la DB debería usar un timeout más corto (ej. 3s) para dejar margen

Esto previene el agotamiento de pools de hilos, fugas de conexiones y mala experiencia de usuario por dependencias no responsivas.

Variantes

VarianteDescripciónCaso de uso
Timeout FijoMismo timeout para todas las llamadasComportamiento simple y predecible
Timeout AdaptativoTimeout basado en latencias históricas (P99)Respuesta dinámica a la salud del servicio
Propagación de DeadlinesPasa el tiempo restante a través de la cadena de llamadasBudgets de latencia end-to-end
Resultados ParcialesDevuelve lo que se obtuvo antes del timeoutStreaming, búsqueda, agregación

Mejores prácticas

  • Siempre establece timeouts en llamadas externas — I/O de red, consultas a base de datos, peticiones HTTP
  • Propaga deadlines a través de tu cadena de llamadas (ej. contexto gRPC, headers HTTP)
  • Establece timeouts más cortos en niveles inferiores — deja margen para reintentos y fallbacks
  • Registra eventos de timeout con el nombre del servicio objetivo para depuración
  • Combina con Circuit Breaker — si los timeouts son frecuentes, deja de llamar al servicio fallido
  • Usa Promise.race en JavaScript y Future.get(timeout) en Java para cancelación limpia

Errores comunes

  • No establecer ningún timeout, permitiendo que las operaciones se cuelguen para siempre
  • Establecer timeouts demasiado largos, derrotando el propósito de fallar rápido
  • Establecer timeouts demasiado cortos, causando fallas innecesarias durante picos normales
  • No cancelar la operación subyacente cuando el timeout se dispara (fugas de recursos)
  • Ignorar la propagación de deadlines, causando misses de deadline en cascada

Preguntas frecuentes

P: ¿Qué valor de timeout debería usar? R: Basalo en tu SLA y en la latencia P99 del servicio descendiente. Si tu API promete 500ms de tiempo de respuesta, y una llamada a DB toma 100ms en P99, establece el timeout de la DB en ~150ms para dejar margen para reintentos y procesamiento.

P: ¿El timeout cancela la operación subyacente? R: Depende de la implementación. La interrupción de hilos señala cancelación pero no la fuerza. Con frameworks async (Java CompletableFuture, JavaScript AbortController), puedes cancelar apropiadamente el I/O subyacente.

P: ¿Debería reintentar después de un timeout? R: Sí, si la operación es idempotente y el timeout podría haber sido causado por un problema transitorio de red. Pero si los timeouts son frecuentes, combínalo con Circuit Breaker para evitar reintentos desperdiciados en un servicio crónicamente lento.