Si administras servicios de alto tráfico en Linux, tarde o temprano te topas con la misma pregunta: ¿sigo con epoll o ya me conviene io_uring? La respuesta no es “usa lo nuevo y ya”. Depende de qué tipo de trabajo hace tu servicio, cuánta latencia tolera, cuántos syscalls estás haciendo por request y cuánto riesgo de migración quieres asumir.
Comparar epoll con io_uring sigue siendo útil porque ambos resuelven problemas parecidos desde ángulos distintos. epoll te da un modelo maduro para esperar eventos de sockets y otros file descriptors. io_uring, en cambio, apunta a reducir el costo de las transiciones al kernel y a unificar varias operaciones de E/S asíncrona en una sola interfaz. Si tu backend maneja miles o cientos de miles de conexiones concurrentes, esa diferencia ya no es teórica.
Qué problema resuelve cada uno
epoll nació para el caso clásico de servidores que necesitan vigilar muchos file descriptors sin bloquear. Lo normal es que registres sockets, esperes eventos y actúes cuando algo cambia. Eso encaja muy bien con servidores HTTP, proxies, chat, colas de mensajes o cualquier app que haga multiplexación de red.
El punto fuerte de epoll es su simplicidad conceptual. Tú registras interés en lectura o escritura, el kernel te avisa cuando hay actividad y tu loop decide qué hacer. Es un modelo probado, estable y muy bien entendido por librerías como libevent, libev y muchas implementaciones de servidores en C, C++ y Rust.
io_uring, por otro lado, no se limita a “esperar eventos”. Su idea es permitir que submits operaciones de E/S a una cola compartida con el kernel y luego recojas completions desde otra cola. Eso abre la puerta a batch submissions, menos syscalls y un flujo más directo para lectura, escritura, accept, timeout y otras operaciones. La documentación oficial del proyecto Linux lo describe como una interfaz de E/S asíncrona basada en rings compartidos entre usuario y kernel: kernel docs.
epoll en una frase
epoll es excelente para saber cuándo un descriptor está listo. No ejecuta la operación por ti; te avisa para que la hagas. Esa separación sigue siendo muy útil cuando tu app ya tiene un event loop maduro y el cuello de botella principal no está en el costo de las syscalls.
io_uring en una frase
io_uring intenta que la E/S ocurra con menos ida y vuelta entre usuario y kernel. En muchos casos, puedes enviar varias operaciones juntas y procesar completions después, lo que reduce overhead en cargas intensivas.
Cómo funcionan realmente
La diferencia más práctica entre ambos está en el flujo mental que le impones a tu código. Con epoll, el patrón suele ser: registrar, esperar, leer o escribir, volver a esperar. Con io_uring, el patrón cambia a: preparar varias solicitudes, enviarlas, recoger completions, encadenar la siguiente tanda.
Eso suena sutil, pero cambia bastante la arquitectura interna. epoll te empuja a un loop de eventos. io_uring te empuja a un modelo de requests y completions. Para cargas con muchas operaciones pequeñas, esa diferencia puede traducirse en menos overhead por operación.
En Linux moderno, io_uring también ha ampliado su alcance. Ya no es solo para reads y writes de archivos. Según la documentación oficial y el desarrollo del kernel, soporta múltiples operaciones, incluyendo networking y timeouts, aunque el soporte exacto depende de la versión del kernel y de cómo esté compilado tu sistema. Si quieres revisar el estado actual, conviene ir a la documentación del kernel y no asumir compatibilidad por defecto.
El ciclo de epoll
Con epoll normalmente haces esto:
- Creas un epoll fd.
- Registras sockets o file descriptors con
epoll_ctl. - Llamas a
epoll_wait. - Procesas los eventos listos.
- Repites.
Ese ciclo funciona bien porque el kernel solo te despierta cuando hay algo que hacer. En un servidor HTTP con keep-alive, por ejemplo, puedes mantener miles de conexiones abiertas sin tener un thread por conexión.
El ciclo de io_uring
Con io_uring, el flujo típico es distinto:
- Inicializas el ring.
- Preparas varias solicitudes en la submission queue.
- Haces submit en batch.
- Lees completions desde la completion queue.
- Encadenas nuevas operaciones según el resultado.
Ese diseño suele rendir mejor cuando puedes agrupar trabajo. Por ejemplo, si un servicio hace muchas lecturas y escrituras pequeñas, o si quieres combinar network I/O con timeouts sin mezclar varios mecanismos distintos, io_uring puede simplificar el camino de ejecución.
Rendimiento: dónde se nota de verdad
La discusión de rendimiento no se resuelve con frases generales. Se resuelve mirando qué costo estás pagando por request. epoll ya es muy eficiente para multiplexación de red, así que no esperes una mejora automática solo por cambiar de API. El beneficio de io_uring aparece cuando tu carga tiene bastante volumen de operaciones y puedes aprovechar batching, completions y menos syscalls.
En la práctica, la pregunta correcta no es “¿io_uring es más rápido?”. La pregunta es “¿mi servicio hace suficiente E/S como para que el overhead de epoll, los syscalls adicionales o el diseño del loop ya me estén costando dinero?”. Si tu app atiende 2,000 requests por segundo, quizá no veas una diferencia clara. Si atiende 200,000 o más y cada request toca red, disco y timers, el análisis cambia.
Hay otro factor: latencia p99. A veces no te interesa tanto el promedio como la cola larga. Si tu servicio tiene picos de latencia por wakeups frecuentes, demasiadas llamadas al kernel o contención en el event loop, io_uring puede ayudar. Pero también puede introducir complejidad operativa y dependencia de una versión concreta del kernel.
Comparación concreta
| Aspecto | epoll | io_uring |
|---|---|---|
| Modelo | readiness-based | completion-based |
| Syscalls por ciclo | normalmente más visibles | puede reducirlas con batch |
| Complejidad mental | más simple | más alta |
| Casos fuertes | sockets, event loops maduros | alto volumen de E/S, batching, mixed I/O |
| Riesgo de migración | bajo | medio a alto |
| Dependencia de kernel | baja, muy extendida | más sensible a versión y features |
Qué medir antes de cambiar
Antes de migrar, mide al menos estas variables:
- p50, p95 y p99 de latencia.
- Syscalls por request.
- Uso de CPU en el thread principal.
- Tiempo en espera versus tiempo útil.
- Throughput sostenido bajo carga real, no solo synthetic benchmarks.
Si no tienes esos datos, estás decidiendo a ciegas. Y en producción, cambiar por intuición sale caro.
Cuándo seguir con epoll
Si tu servicio ya usa epoll y cumple sus objetivos de latencia y throughput, no tienes una razón fuerte para migrar solo por tendencia. epoll sigue siendo una opción sólida para servidores de red, especialmente cuando tu aplicación ya está afinada alrededor de un event loop y las ganancias esperadas de io_uring no compensan el trabajo de reescritura.
También conviene quedarte con epoll si tu equipo necesita portabilidad operacional dentro del ecosistema Linux más común. Muchas distribuciones, kernels y stacks de producción ya están muy bien soportados con epoll. Eso reduce sorpresas al desplegar en proveedores distintos o en entornos híbridos.
Otro caso claro: si tu carga principal es I/O de red relativamente simple, con un patrón de lectura-escritura bien resuelto, epoll puede ser suficiente. No necesitas cambiar una base estable por una API más nueva si no tienes un problema medible que resolver.
Señales de que epoll te alcanza
- Tu latencia p99 ya está dentro de tu SLO.
- El CPU no está saturado por syscalls.
- El event loop no es tu cuello de botella.
- Tu equipo no tiene tiempo para una migración con pruebas extensas.
- Tu kernel o tu entorno de despliegue no están estandarizados para io_uring.
En otras palabras, si el sistema funciona y no tienes una métrica clara que mejorar, epoll sigue siendo una apuesta razonable.
Cuándo vale la pena io_uring
io_uring empieza a tener sentido cuando tu servicio hace mucha E/S y el costo del modelo actual ya se ve en números. Piensa en proxies de alto tráfico, storage gateways, bases de datos, servicios de streaming, pipelines de ingestión o backends que combinan red, disco y timeouts con mucha frecuencia.
El beneficio real suele aparecer cuando puedes agrupar trabajo. Por ejemplo, en vez de disparar una syscall por cada operación, puedes preparar varias solicitudes y dejarlas correr. Eso reduce overhead y puede mejorar el throughput. También puede ayudar si quieres unificar networking y file I/O bajo una sola abstracción.
No obstante, io_uring no es una bala de plata. Su adopción exige revisar la versión del kernel, los flags disponibles, el soporte de tu lenguaje o runtime y el comportamiento bajo backpressure. Si usas Rust, C o C++, el ecosistema ya tiene opciones bastante maduras. Si dependes de bindings más jóvenes, revisa bien la calidad de la integración.
Casos donde sí suele pagar
- Tienes un servicio con muchísimas operaciones pequeñas y repetitivas.
- Tu p99 empeora por overhead de scheduling o wakeups frecuentes.
- Necesitas combinar I/O de red y disco con menos capas intermedias.
- Tu equipo puede probar en staging con carga real durante varias semanas.
- Controlas el kernel de producción y puedes fijar versiones compatibles.
Casos donde puede salir caro
- Tu app es estable y el problema no es el event loop.
- Tu infraestructura tiene kernels desparejos.
- Usas una librería que todavía no abstrae bien io_uring.
- No tienes observabilidad suficiente para comparar antes y después.
- Tu equipo no quiere mantener dos caminos de E/S durante la transición.
Cómo decidir sin adivinar
La forma más práctica de decidir es hacer una evaluación por etapas. No migres toda la base de una vez. Toma un servicio representativo, mide y compara con números reales. Si el cambio no mejora tus métricas, no hay razón para forzarlo.
Un enfoque razonable sería este:
- Identifica el cuello de botella real con profiling y métricas de producción.
- Cuenta syscalls por request o por conexión activa.
- Mide p95 y p99 en una ventana de carga estable.
- Haz un prototipo con io_uring en una ruta concreta, no en todo el sistema.
- Compara CPU, latencia, throughput y complejidad de operación.
- Decide si el beneficio supera el costo de mantenimiento.
Si estás en una empresa en Latinoamérica, donde muchas veces el equipo es pequeño y la presión por entregar es alta, este orden importa todavía más. Una migración técnica que mejora 8 por ciento el throughput pero añade semanas de mantenimiento puede no ser una buena inversión. En cambio, si tu servicio está cerca del límite de CPU y cada punto porcentual cuenta, sí vale la pena el trabajo.
Un ejemplo realista
Imagina un servicio de ingestión de eventos que recibe 50,000 mensajes por segundo, escribe lotes a disco y responde a clientes por HTTP. Con epoll, puedes manejar la red sin problemas, pero quizá termines con más lógica alrededor de threads auxiliares, colas internas y syscalls separadas para timers y escrituras. Con io_uring, podrías simplificar parte del pipeline y reducir overhead si el kernel y el runtime acompañan.
Ahora imagina un API backend de una fintech con 5,000 requests por segundo, mostly CRUD, caches y consultas a una base externa. Ahí el cuello de botella puede estar en la base de datos o en la red externa, no en el mecanismo de multiplexación. Migrar a io_uring probablemente no te dará una mejora visible.
Tabla resumen
| Pregunta | Respuesta corta |
|---|---|
| ¿epoll está obsoleto? | No, sigue siendo una opción sólida y muy usada. |
| ¿io_uring siempre rinde más? | No, depende de la carga y del kernel. |
| ¿Cuándo conviene epoll? | Cuando tu event loop ya funciona bien y la latencia cumple. |
| ¿Cuándo conviene io_uring? | Cuando haces mucha E/S y puedes aprovechar batching. |
| ¿Qué debo medir primero? | p95, p99, syscalls por request y CPU. |
| ¿La migración es trivial? | No, requiere pruebas, compatibilidad y observabilidad. |
Fuentes y documentación útil
Si quieres ir a la fuente antes de tocar producción, revisa la documentación oficial del kernel sobre io_uring: kernel docs. También vale la pena leer la referencia de epoll en la documentación de Linux: man7 epoll. Si trabajas con Rust, la guía de Tokio sobre I/O asíncrona te ayuda a entender cómo se abstraen estos mecanismos en un runtime moderno: Tokio docs.
La decisión final no debería basarse en simpatía por una API. Debería basarse en métricas, compatibilidad y costo de operación. epoll sigue siendo una herramienta excelente para muchos servicios. io_uring vale la pena cuando tu carga lo justifica y tu equipo puede absorber la complejidad extra.
Si hoy tu sistema ya está estable, probablemente no necesites cambiar nada. Si estás persiguiendo menos syscalls, mejor p99 y mejor uso de CPU en un servicio de alto rendimiento, entonces sí tienes un caso para evaluar io_uring con cuidado.
Preguntas frecuentes
¿epoll y io_uring resuelven exactamente lo mismo?
¿io_uring reemplaza a epoll en todos los servidores Linux?
¿Qué métrica me dice si debo migrar?
¿Necesito un kernel nuevo para usar io_uring?
¿Puedo usar io_uring para red y disco al mismo tiempo?
¿Qué tipo de servicio suele beneficiarse más?
¿Vale la pena migrar solo por modernizar el stack?
Azirgo
¿Listo para construir tu Producto Digital?
Sitios web, apps móviles, software a medida y soluciones blockchain. Cuéntanos qué tienes en mente y armamos un plan claro contigo.
- Cotización clara en 48 horas
- Equipo en Ecuador, atención en español
- Desde un MVP hasta un producto en producción