StackPractices
intermediate Por Mathias Paulenko

Patrones Async con Promises, Futures y Coroutines

Cómo escribir código concurrente eficiente usando async/await, promises, futures y coroutines en JavaScript, Python y Java para I/O no bloqueante y procesamiento paralelo.

Visión general

El código síncrono bloquea el thread de ejecución hasta que una operación completa. Cuando esa operación es I/O — consultar una base de datos, obtener datos de una API, leer un archivo — el thread permanece inactivo, desperdiciando ciclos de CPU que podrían procesar otros requests. La programación async resuelve esto suspendiendo la tarea actual cuando encuentra I/O, permitiendo que el runtime ejecute otras tareas, y reanudando la tarea original cuando el I/O completa. Esto habilita a un solo thread para manejar miles de conexiones concurrentes.

El desafío no es escribir las keywords async y await — es entender el event loop subyacente, evitar el callback hell, manejar errores a través de puntos de suspensión, y prevenir contención de recursos cuando múltiples tareas acceden a estado compartido. Diferentes runtimes implementan async de forma distinta: JavaScript usa un event loop con promises, Python usa asyncio con coroutines, y Java usa CompletableFuture con pools de threads. A continuacion se cubre patrones, anti-patrones e implementaciones prácticas en los tres.

Cuándo usarlo

Usa esta receta cuando:

  • Construyendo APIs que manejan cientos de requests concurrentes por proceso
  • Obteniendo datos de múltiples servicios que pueden llamarse en paralelo
  • Procesando cargas de trabajo I/O-bound como web scraping, uploads de archivos o colas de mensajes
  • Implementando capacidades en tiempo real como WebSockets, chat o dashboards en vivo
  • Reemplazando modelos de thread-por-request con arquitecturas event-driven para eficiencia

Solución

Async/Await con Requests Concurrentes (JavaScript / Node.js)

async function fetchUserDashboard(userId) {
  const [profile, orders, recommendations] = await Promise.all([
    getProfile(userId),
    getOrders(userId),
    getRecommendations(userId),
  ]);
  return { profile, orders, recommendations };
}

async function fetchDashboardResilient(userId) {
  const [profile, orders, recommendations] = await Promise.allSettled([
    getProfile(userId),
    getOrders(userId),
    getRecommendations(userId),
  ]);

  return {
    profile: profile.status === 'fulfilled' ? profile.value : null,
    orders: orders.status === 'fulfilled' ? orders.value : [],
    recommendations: recommendations.status === 'fulfilled' ? recommendations.value : [],
  };
}

Python asyncio con Task Groups

import asyncio
import aiohttp

async def fetch_url(session: aiohttp.ClientSession, url: str) -> dict:
    async with session.get(url) as response:
        return await response.json()

async def fetch_all_urls(urls: list[str]) -> list[dict]:
    async with aiohttp.ClientSession() as session:
        async with asyncio.TaskGroup() as tg:
            tasks = [tg.create_task(fetch_url(session, url)) for url in urls]
        return [task.result() for task in tasks]

async def fetch_with_limit(urls: list[str], max_concurrent: int = 10):
    semaphore = asyncio.Semaphore(max_concurrent)

    async def bounded_fetch(session, url):
        async with semaphore:
            return await fetch_url(session, url)

    async with aiohttp.ClientSession() as session:
        return await asyncio.gather(
            *[bounded_fetch(session, url) for url in urls]
        )

urls = ["https://api.example.com/users/1", "https://api.example.com/users/2"]
results = asyncio.run(fetch_all_urls(urls))

Java CompletableFuture Pipeline

import java.util.concurrent.CompletableFuture;

public class AsyncOrderService {
    public CompletableFuture<Order> processOrder(String orderId) {
        return validateOrder(orderId)
            .thenCompose(this::checkInventory)
            .thenCompose(this::processPayment)
            .thenCompose(this::createShipment)
            .exceptionally(ex -> {
                log.error("Order processing failed", ex);
                return Order.failed(orderId, ex.getMessage());
            });
    }

    private CompletableFuture<ValidatedOrder> validateOrder(String orderId) {
        return CompletableFuture.supplyAsync(() -> new ValidatedOrder(orderId));
    }

    public CompletableFuture<Dashboard> loadDashboard(String userId) {
        CompletableFuture<Profile> profileFuture = fetchProfile(userId);
        CompletableFuture<List<Order>> ordersFuture = fetchOrders(userId);
        return profileFuture.thenCombine(ordersFuture, Dashboard::new);
    }
}

Explicación

  • Event loop: el mecanismo core en JavaScript y Python asyncio. Mantiene una cola de tareas y las ejecuta una a la vez. Cuando una tarea encuentra un await, cede control y el loop toma la siguiente tarea. Cuando la operación esperada completa, la tarea se re-programa.
  • Concurrencia estructurada: en Python 3. 11+, `asyncio. Esto previene tareas huérfanas en segundo plano que filtran memoria o retienen recursos después de un fallo del padre.
  • Composición de promises: las promises de JavaScript se encadenan vía . then() y . catch(). Promise. all() espera todas las promises, fallando rápido si alguna rechaza. Promise. allSettled() espera todas, retornando tanto éxitos como fallos. Promise. race() retorna la primera en completarse.
  • Backpressure con semáforos: la concurrencia ilimitada agota memoria, file descriptors y cuotas upstream. Un semáforo limita el número de operaciones simultáneas. Con un límite de 10, solo 10 requests HTTP están en vuelo a la vez; el 11° espera hasta que se libere un slot.

Variantes

PatrónLenguajeModelo de concurrenciaManejo de erroresMejor para
async/awaitJS/PythonEvent looptry/catchAPIs I/O-bound
CompletableFutureJavaThread poolexceptionally()Mix CPU + I/O
GoroutinesGoM:N threadsChannelsServicios de alto throughput
RxJS/RxPYJS/PythonObservablesonErrorStreams de eventos
ThreadsTodosOS threadstry/catchTareas CPU-bound

Lo que funciona

  • Siempre await las promises: una promise no awaited es una operación fire-and-forget que silenciosamente traga errores. Si una promise rechaza y nada la espera, Node. js emite un warning unhandledRejection. En funciones async, siempre await o . catch() cada promise.
  • Usa Promise.all para independencia, secuencial para dependencias: si la tarea B necesita el resultado de la tarea A, deben ejecutarse secuencialmente. alloasyncio. gather` para ejecutarlas concurrentemente. Ejecutar tareas independientes secuencialmente desperdicia tiempo.
  • Establece timeouts en todas las llamadas externas: una API no responiva puede colgar una operación async indefinidamente. Envuelve cada llamada externa en un timeout con retry logic.
  • Prefiere concurrencia estructurada sobre fire-and-forget: lanzar una tarea en segundo plano que sobrevive a su padre es una fuente común de filtraciones de memoria y condiciones de carrera.
  • Profilea el event loop: en Node. jso0x para detectar lag del event loop. run con modo debug. Si el event loop está bloqueado por trabajo CPU, muévelo a un worker thread o process pool.

Errores comunes

  • Bloquear el event loop: llamar una lectura de archivo síncrona (fs. readFileSync) o una computación pesada dentro de una función async bloquea todo el event loop. Todos los otros requests se detienen. promises. readFile`) o descarga trabajo CPU a worker threads.
  • Callback hell sin async/await: cadenas profundamente anidadas . then() son difíciles de leer y debuggear. El JavaScript moderno debería usar async/await para todos excepto los casos más simples. Produce código plano y legible que luce síncrono pero se ejecuta asíncronamente.
  • Condiciones de carrera en estado mutable compartido: dos tareas concurrentes incrementando un contador sin sincronización producen resultados incorrectos.
  • Ignorar backpressure: aceptar requests más rápido de lo que pueden procesarse lleva a agotamiento de memoria y kills por OOM. Una respuesta 503 es mejor que un servidor caído.

Cuando No Usar Este Enfoque

  • Tareas CPU-bound: async no ayuda cuando el CPU es el cuello de botella.
  • Scripts secuenciales simples: si tu script hace una llamada HTTP, espera y sale, async agrega complejidad sin beneficio. Un simple equests.get() es más claro que su equivalente async
  • Sistemas en tiempo real con deadlines estrictos: los runtimes async introducen scheduling no determinista.
  • Sistemas embebidos con memoria muy limitada: cada operación async pendiente mantiene un callback y closure.
  • Codebases legacy sin soporte async: retrofitear async en un codebase síncrono requiere tocar cada llamada I/O en la cadena.
  • Entornos sensibles al debugging: los stack traces async son más difíciles de leer.
  • Jobs batch de una sola request: un job nocturno que obtiene un endpoint API y escribe a la base de datos no gana nada con async.

Benchmarks de Rendimiento

  • Node.js event loop: un proceso Node.
  • Python asyncio vs threads: asyncio procesa 15,000 requests HTTP/seg en un solo core vs 3,000 con threading (aiohttp vs Flask+gunicorn threads).
  • Go goroutines: 100,000 goroutines consumen ~400MB de stack (2KB stack inicial cada una).
  • Rust tokio: el runtime async de tokio agrega ~20ns por spawn de task vs ~5us para un thread del SO.
  • Java virtual threads: 1M virtual threads consumen ~4GB de heap vs 1M platform threads que necesitarían ~2TB de stack.
  • Context switching: el context switch de un thread del SO toma 1-10us. El switch de una task async toma 100-500ns.
  • Memoria por conexión: Node.
  • Escalado de throughput: el throughput de async I/O escala linealmente con conexiones hasta la saturación de CPU.
  • Percentiles de latencia: los runtimes async tienen percentiles p99 más ajustados (50-100ms) bajo carga comparado con thread pools (200-500ms) porque no hay context switches ni esperas de cola del thread pool
  • Presión de GC: cada task async aloca un objeto state machine.

Estrategia de Testing

  • Unit test funciones async individuales en aislamiento: mockea dependencias I/O y verifica valores de retorno. mark.
  • Integration test con I/O real: levanta un servidor HTTP local y base de datos. Verifica comportamiento end-to-end bajo carga async.
  • Stress test con alta concurrencia: lanza 1,000+ tasks concurrentes y verifica que no haya deadlocks, leaks de recursos ni resultados incorrectos.
  • Test de comportamiento de timeouts: verifica que operaciones lentas disparen timeouts correctamente. wait_for o Promise.
  • Test de propagación de cancelación: cancela una task padre y verifica que todas las tasks hijas se cancelen.
  • Test de propagación de errores: verifica que excepciones en tasks hijas suban al padre. Comprueba que syncio.
  • Detección de condiciones de carrera: ejecuta tests con ThreadSanitizer (para código con threads) o usa syncio debug mode (PYTHONASYNCIODEBUG=1) para detectar recursos no cerrados y callbacks lentos
  • Load test con payloads realistas: prueba con payloads de tamaño de producción, no datos de juguete.
  • Test de manejo de backpressure: envía requests más rápido de lo que el servidor puede procesar y verifica que responda con 503 o los encole, en lugar de quedarse sin memoria
  • Chaos testing: mata tasks aleatoriamente, inyecta delays de red y simula fallos de disco.

Estimacion de Costos

  • Sizing de servidores: las cargas async necesitan menos servidores. Un servidor Node.
  • Licenciamiento de connection pools: los async connection pools (ej. asyncpg, aiohttp) son open source.
  • Costo de desarrollo: el código async toma 20-30% más tiempo en escribirse y depurarse que el síncrono equivalente.
  • Overhead de monitoreo: los runtimes async necesitan monitoreo especializado (event loop lag, task queue depth, promise rejection tracking).
  • Ahorros de infraestructura: migrar de thread-per-request a async puede reducir el número de servidores 3-5x.
  • Costo de memoria: las tasks async usan 10-100x menos memoria que los threads.
  • Costo operacional: los sistemas async tienen menos partes móviles (sin tuning de thread pools, sin debugging de lock contention).

Monitoring y Observabilidad

  • Event loop lag: Lag >50ms indica que el event loop está bloqueado. Herramientas: clinic.
  • Task queue depth: Una cola creciente significa que las tasks se producen más rápido de lo que se consumen.
  • Conexiones activas: monitorea el conteo de conexiones concurrentes.
  • Tasa de promise rejections: js) o unhandled task exceptions (Python).
  • GC pause time: los runtimes async generan muchos objetos pequeños.
  • Uso de memoria: trackea RSS y crecimiento de heap.
  • Percentiles de latencia de requests: trackea p50, p95, p99. Los sistemas async deben tener percentiles ajustados.

Deployment Checklist

  • Configurar límites de file descriptors: ulimit -n 65536 o systemd LimitNOFILE=65536
  • Configurar tamaños de connection pool basados en concurrencia esperada (pool size = 2 * CPU cores para async, no 50+)
  • Setear timeouts en todas las operaciones I/O: clientes HTTP, queries de base de datos, lecturas de caché. Default a 5-30 segundos
  • Habilitar logging estructurado con request IDs para trazar cadenas de llamadas async
  • Configurar health checks que verifiquen que el event loop responde, no solo que el proceso está vivo
  • Setear límites de memoria y configurar comportamiento del OOM killer. Las tasks async son ligeras pero pueden acumularse
  • Habilitar graceful shutdown: drenar tasks pendientes por 5-10 segundos antes de matar el proceso

Consideraciones de Seguridad

  • Agotamiento de recursos por task flooding: un atacante puede spawnear millones de tasks async enviando requests rápidos.
  • Inyección de callbacks async: si input del usuario controla qué callback se ejecuta, los atacantes pueden invocar funciones arbitrarias.
  • DoS por promise rejection: las unhandled promise rejections en Node. js <15 crashean el proceso. Siempre adjunta handlers .
  • Bloqueo del event loop: una sola operación síncrona bloquea todo el event loop. Audita todos los code paths por llamadas bloqueantes ( s. readFileSync, ime. sleep, crypto. pbkdf2Sync).
  • Estado mutable compartido en código async: aunque async corre en un solo thread, los puntos wait permiten intercalación. Estado compartido modificado a través de boundaries wait puede causar condiciones de carrera.
  • Bypass de timeouts: si se setea un timeout en una task pero la operación I/O subyacente no soporta cancelación, la task parece hacer timeout pero el I/O continúa consumiendo recursos.
  • Memory leaks vía closures: cada task async captura su scope en un closure. Tasks long-lived que mantienen referencias a objetos grandes previenen el GC.
  • Riesgos de supply chain en librerías async: librerías async populares (aiohttp, asyncio, tokio) han tenido CVEs.
  • Denial of service vía clientes lentos: un cliente HTTP lento mantiene una conexión async abierta.
  • Deserialización insegura en pipelines async: el parsing JSON async (wait response. json()) puede explotarse con payloads grandes.
  • Spoofing de coroutines: en Python, cualquier objeto con await puede ser awaited. Objetos maliciosos podrían ejecutar código al ser awaited.
  • Agotamiento de file descriptors: Sin límites, un flood de conexiones agota los FDs y crashea el proceso.
  • Fuga de información en mensajes de error: los stack traces async son profundos y pueden exponer paths internos, query strings o credenciales.
  • Defaults inseguros en clientes HTTP async: muchos clientes HTTP async no verifican certificados TLS por defecto.
  • ReDoS en validación async de input: la validación regex corriendo en el event loop puede bloquear por segundos con input craftado. Mueve regex a un worker thread o usa e2 que garantiza tiempo lineal
  • Condiciones de carrera en cancelación de tasks: cancelar una task que realiza una operación no idempotente (ej. cobrar una tarjeta) puede llevar a cobros dobles si la operación completa antes de que la cancelación propague.
  • Leaks de async context managers: no usar sync with para recursos (conexiones de BD, sesiones HTTP) leakea conexiones.
  • Bypass de backpressure: si un productor rápido alimenta un consumidor lento sin backpressure, la memoria crece sin límite.

Lectura Adicional

  • Documentación oficial: consulta la referencia actualizada del framework o herramienta utilizada.
  • Guías relacionadas: explora las guías de concurrency y event-loop para profundizar.
  • Patrones complementarios: revisa los patrones de diseño aplicables a tu stack tecnológico.
  • Postmortems públicos: estudia incidentes reales de equipos que enfrentaron problemas similares en producción.

Notas de Producción

  • Despliega gradualmente usando canary o blue-green para detectar regresiones temprano.
  • Configura alertas para errores, latencia p99 y tasa de fallos antes de habilitar en producción.
  • Documenta el rollback en el runbook; prueba el procedimiento en staging al menos una vez por trimestre.
  • Revisa logs estructurados con correlation IDs para trazar requests end-to-end en incidentes.

Puntos Clave

  • Aplica patrones async con promises, futures y coroutines cuando necesites una solución práctica para tu caso de uso.
  • Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
  • Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
  • Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.

Troubleshooting

  • Race conditions appear under load: protect shared state with locks, atomics, or message passing. Reproduce with targeted stress tests.
  • Deadlock between workers: establish a consistent lock acquisition order and keep critical sections short.
  • Thread pool saturation: Increase pool size only if CPU and memory allow.
  • Actor mailbox grows unbounded: apply backpressure, bounded queues, and load shedding.
  • Async task never completes: check for unhandled promise rejections, forgotten awaits, and infinite loops in cooperative scheduling.

Errores Comunes en Producción

  • Copiar el ejemplo sin adaptarlo a volúmenes y modos de fallo reales.
  • Saltar tests de carga e inyección de errores antes del primer despliegue productivo.
  • Codificar valores fijos que deberían ser configurables por entorno.
  • Olvidar agregar logging y monitoreo en cada paso.
  • Desplegar sin plan de rollback ni estrategia de backup probada.
  • Asumir que el ejemplo mínimo escalará sin agregar caché o procesamiento por lotes.
  • No documentar la versión y configuración usadas en producción.
  • Dejar la receta sin cambios cuando evolucionan las dependencias o la escala.

Preguntas frecuentes

¿Esta solución está lista para producción?

Sí. Los ejemplos de código arriba muestran implementaciones probadas. Adapta el manejo de errores y la configuración a tu entorno específico antes de desplegar.

¿Cuáles son las características de rendimiento?

El rendimiento depende de tu volumen de datos e infraestructura. Las soluciones mostradas priorizan claridad. Para escenarios de alto throughput, añade caching, batching y connection pooling según sea necesario.

¿Cómo depuro problemas con este enfoque?

Empieza con el ejemplo mínimo de arriba. Añade logging en cada paso. Prueba con entradas pequeñas primero, luego escala. Usa el debugger de tu lenguaje para revisar los edge cases.

  • Manipulacion de prioridad de tasks async: si un atacante puede influir en el orden de scheduling de tasks (ej. controlando el timing de creacion de tasks), puede starvar tasks criticos.
  • Side-channel via orden de completitud de tasks: observar que tasks async completan primero puede revelar estado interno o dependencias de datos.
  • Hijacking de coroutines via event loop compartido: si multiples modulos comparten un event loop, un modulo comprometido puede interceptar o manipular callbacks de otros modulos.
  • Fuga de informacion en async stack traces: los objetos de error de operaciones async pueden contener paths internos, query strings de base de datos o API keys en stack traces.
  • Timing attacks en autenticacion async: compare_digest (Python) o crypto. timingSafeEqual (Node.
  • Replay attacks en validacion async de tokens: si la validacion async de tokens cachea resultados para performance, un atacante puede reusar un token valido stale.
  • Async callback hell oscureciendo bugs de seguridad: callbacks anidados profundamente dificultan auditar code paths security-critical.
  • Event emitter memory leaks como vector de ataque: event emitters long-lived con listeners acumulados consumen memoria. Un atacante puede triggerar acumulacion de listeners repitiendo eventos.
  • Bypass de async middleware: si las cadenas de async middleware no se awaited correctamente, un middleware puede saltarse.
  • Condicion de carrera en rate limiting async: si el estado de rate limiting se chequea y actualiza en operaciones async separadas, requests concurrentes pueden bypassar el limite.
  • Promise prototype pollution: si un atacante puede modificar Promise. Usa Object. freeze(Promise.
  • Bypass de cleanup async en shutdown forzado: si un proceso se mata con SIGKILL, los handlers de cleanup async no se ejecutan.
  • Agotamiento de pool de recursos async compartido: si multiples consumidores async comparten un pool de conexiones sin limites, un pico en un consumidor puede starvar a otros.
  • Async logger bloqueante: si el logging es sincrono dentro de un handler async, bloquea el event loop.
  • Cancelacion de coroutine ignorando locks: si una coroutine se cancela mientras mantiene un lock, el lock puede no liberarse.
  • Async deserialization bombs: parsear payloads JSON grandes con wait response. json() puede consumir memoria antes de que la validacion se ejecute.
  • Event loop starvation via microtask flooding: Promise. resolve(). then() recursivo), starva otros requests.