- zeroserve, un servidor HTTPS pequeño y rápido, recibe un tarball de sitio web y lo sirve con HTTP/2 y TLS 1.3, ejecutando además en cada solicitud los programas eBPF incluidos en el tarball como middleware en sandbox en espacio de usuario
- Sin archivo de configuración, un programa eBPF decide por solicitud el enrutamiento, los encabezados, la autenticación, el rate limiting y el proxy, unificando la configuración declarativa de nginx y Caddy con una capa separada de scripting
- El sitio se indexa como un único archivo tar y no se extrae al disco; al reemplazar el tarball y enviar
SIGHUP, se sustituyen de forma atómica el sitio, los scripts y los materiales TLS sin perder conexiones
- En benchmarks HTTPS de un solo núcleo, zeroserve registró 36,681 req/s con archivos estáticos pequeños, 46,945 req/s con JSON dinámico eBPF de 10 ms y 26,486 req/s como proxy pequeño, pero en proxy de 100 KB nginx llevó ventaja con 5,882 req/s
- zeroserve busca ser una alternativa a nginx y Caddy combinando despliegue en un solo tarball, configuración programática, eBPF en espacio de usuario y TLS moderno, aunque para respuestas proxy grandes nginx sigue siendo más adecuado
Resumen
- zeroserve es un servidor HTTPS pequeño, rápido y sin configuración que sirve un único tarball de sitio web mediante HTTP/2 y TLS 1.3
- Los programas eBPF incluidos dentro del tarball se ejecutan en cada solicitud como middleware en sandbox en espacio de usuario, y pueden manejar reescritura de solicitudes, autenticación, rate limiting y reverse proxy hacia backends
- Es un servidor diseñado para superar a nginx en un solo núcleo en la mayoría de las cargas de trabajo con archivos estáticos pequeños y grandes, middleware con scripts y proxy de respuestas pequeñas
- Los scripts eBPF se compilan JIT a código nativo y se aíslan en espacio de usuario, con el objetivo de que su costo sea lo bastante bajo como para ejecutarse en cada solicitud
- Las operaciones de red y disco se envían mediante
io_uring a través del runtime monoio
- Soporta TLS 1.3, HTTP/2, Encrypted Client Hello, selección de certificado por SNI y fingerprinting JA4
- Todo el sitio y el material TLS se sirven desde un solo tarball y pueden recargarse en caliente con
SIGHUP
Modelo de configuración: el programa es la configuración
- zeroserve apunta a ser una alternativa a nginx y Caddy, y su decisión central de diseño está en el modo de configuración
- nginx y Caddy ofrecen lenguajes de configuración declarativos como bloques
location, reglas rewrite, directivas map y try_files, y cuando llegan a sus límites les añaden al lado un runtime opcional de scripting como Lua o plugins de Caddy
- En esa estructura, el comportamiento se divide entre una capa de directivas con su propio flujo de control y una capa de scripts que se ejecuta en puntos específicos del ciclo de vida de la solicitud
- zeroserve no tiene archivo de configuración: un solo programa eBPF ve todas las solicitudes y decide el enrutamiento, los encabezados, la autenticación, el rate limiting y el proxy
Servir directamente un solo tarball
- Todo el sitio es un único archivo
tar, y al cargarlo zeroserve construye un mapa path -> byte-range y sirve archivos leyendo rangos de bytes directamente del propio tarball
- Como no se extrae ningún archivo al disco, el sitio existe solo dentro de un único archivo y no hay document root que una regla
location mal hecha pueda exponer
- El despliegue consiste en el reemplazo atómico de un solo archivo, y publicar una nueva versión implica sustituir el tarball y luego enviar
SIGHUP
- El empaquetado del directorio y el comando de ejecución tienen esta forma
zeroserve --pack ./public > site.tar
zeroserve --addr 0.0.0.0:8080 site.tar
- El comando de recarga en caliente tiene esta forma
killall -SIGHUP zeroserve
- La recarga reemplaza de forma atómica el sitio, los scripts y el material TLS dentro del mismo proceso, y funciona sin perder conexiones
- Cada instancia es un event loop de un solo hilo; esto es una limitación por proceso, pero se presenta como una forma adecuada cuando la unidad de escalado es “más procesos”
Scripting eBPF en espacio de usuario
- Todos los archivos
.c colocados bajo .zeroserve/scripts/ se compilan a objetos eBPF con clang y llc al momento del empaquetado, y se ejecutan en cada solicitud
- eBPF no corre sobre el subsistema BPF del kernel ni requiere
CAP_BPF, sino que se ejecuta en espacio de usuario dentro del runtime async-ebpf en un proceso normal sin privilegios
- async-ebpf integra uBPF y compila JIT el bytecode a código máquina nativo x86-64
- El pointer cage enmascara todos los accesos a memoria del código compilado JIT dentro de una arena dedicada al programa, confinando los accesos inválidos dentro de la memoria del script
- Los scripts se ejecutan directamente en el event loop único de zeroserve, y para evitar que un script lento detenga otras conexiones, un temporizador puede interrumpir el código nativo compilado JIT en plena ejecución y devolver el control al event loop
- El modelo de programación es una cadena de scripts ejecutados según el orden alfabético de los nombres de archivo, y los scripts comparten un mapa de metadatos por solicitud
- Si un script llama a
zs_respond o zs_reverse_proxy, la cadena se corta de inmediato
- Las claves bajo
zs.response.header.* se convierten en encabezados de todas las respuestas, y otras claves se usan en una pequeña pasada de plantillas que reemplaza placeholders como <zs-meta>visitor</zs-meta> en archivos HTML al momento de salida
- La superficie de helpers permite leer método, ruta, query, encabezados y dirección del peer de la solicitud, así como reescribir URI y establecer o eliminar encabezados
- Los helpers criptográficos y de codificación ofrecen SHA-256, HMAC-SHA256, base64, hex y
getrandom
- Los helpers JSON permiten parsear el cuerpo de la solicitud, crear y modificar árboles de documentos y responder con
zs_json_respond
- El rate limiting soporta token buckets basados en claves arbitrarias como IP del peer o API key, y el estado se conserva incluso después de una recarga en caliente
- Los helpers de AWS SigV4 soportan encabezados
Authorization firmados y URLs presignadas para comunicarse con S3 y otros servicios de AWS
- El inicio de sesión OIDC ofrece un flujo de relying party basado en Authorization Code + PKCE, y guarda toda la sesión en cookies sealed con XChaCha20-Poly1305 para mantener el servidor stateless mientras se pone un sitio estático detrás de “Iniciar sesión con Google”
- Los endpoints dinámicos responden directamente desde scripts en rutas específicas; en el ejemplo, una solicitud a
/health devuelve el encabezado application/json y el cuerpo {"status":"ok"}
- Cada script se ejecuta con un límite predeterminado de 256 KB de memoria, y el runtime reparte el tiempo de ejecución de scripts largos y limita los que se descontrolan
- Los scripts pueden llamarse entre sí con
zs_call, y la profundidad de llamada está limitada
- Un script atrapado en un bucle infinito solo retrasa su propia solicitud; el temporizador preventivo lo interrumpe para que el servidor siga atendiendo otras solicitudes
- La capa TLS es exclusivamente TLS 1.3 y termina con BoringSSL
- Encrypted Client Hello evita que el SNI real aparezca en texto plano, y además ofrece selección de certificados SNI basada en directorios y fingerprinting de cliente JA4 expuesto a los scripts
- El modo de relay ECH transparente pasa byte por byte al upstream real cualquier handshake que no pueda descifrarse, permitiendo que nombres protegidos se mezclen detrás de un nombre público
Rendimiento
-
Condiciones del benchmark
- Se comparó HTTPS sirviendo el mismo contenido y el mismo certificado autofirmado entre zeroserve, nginx 1.26 y Caddy 2.11 en un Ryzen 7 3700X de 8 núcleos
- Como una instancia de zeroserve es monohilo por diseño, el criterio de comparación es el rendimiento por núcleo
- Todos los servidores se fijaron a una sola CPU con
taskset; nginx usó worker_processes 1, Caddy usó GOMAXPROCS=1 y zeroserve mantuvo su estructura monohilo existente
- La carga se generó desde otros núcleos con
wrk -t4 -c100, usando la mediana de 3 ejecuciones de 10 segundos
wrk usa HTTP/1.1, por lo que las cifras reflejan HTTP/1.1 sobre TLS 1.3, es decir, el costo en estado estable de conexiones HTTPS ya abiertas con conexiones keep-alive largas que amortizan el costo del handshake
-
Archivo estático pequeño de 174 B
| Servidor |
req/s |
p99 |
| zeroserve |
36,681 |
5.4 ms |
| nginx |
31,226 |
7.8 ms |
| Caddy |
12,830 |
22 ms |
- zeroserve sirvió archivos pequeños cerca de un 17% más rápido que nginx en un solo núcleo, y también con menor latencia de cola
- Casos base de sitios estáticos como páginas HTML, JSON pequeños y CSS forman parte del objetivo de tuning de zeroserve
-
Archivo estático grande de 100 KB
| Servidor |
req/s |
Throughput |
p99 |
| zeroserve |
8,000 |
782 MB/s |
22 ms |
| nginx |
7,600 |
773 MB/s |
28 ms |
| Caddy |
6,084 |
590 MB/s |
44 ms |
- Los resultados de los tres servidores fueron cercanos, con zeroserve apenas al frente en un solo núcleo con unos 780 MB/s
- La ventaja de nginx con archivos grandes mediante
sendfile() no se usa bajo TLS, ya que los bytes deben cifrarse en espacio de usuario, así que los tres servidores quedan atados al cifrado y al loop de escritura
- Con kernel TLS desactivado en los tres, la ruta de lectura y escritura con
io_uring de zeroserve fue ligeramente más rápida
eBPF vs Lua
- La comparación de scripting se hizo contra nginx + LuaJIT
ngx_http_lua_module, una forma común de ejecutar código rápido dentro de un servidor web
- zeroserve configura por defecto el temporizador preventivo de scripts cada 2 ms; un intervalo fino acelera la limitación de scripts problemáticos, pero también añade costo a los scripts normales
- Con el valor predeterminado de 2 ms, eBPF queda en unos 32k req/s en respuestas totalmente dinámicas, por debajo de los 41k req/s de nginx Lua
- Al subir
--preempt-timer-interval-ms a 10, el throughput de scripting se recupera cerca de un 40% y el resultado se invierte
-
Middleware de inyección de encabezados por solicitud
| Motor |
req/s |
p99 |
| zeroserve eBPF 10 ms |
43,709 |
5.1 ms |
| zeroserve eBPF 2 ms por defecto |
31,334 |
6.7 ms |
nginx Lua header_filter |
28,653 |
8.4 ms |
- En este caso de middleware, donde el script se ejecuta pero el archivo estático se sigue sirviendo, eBPF a 10 ms supera a nginx Lua por cerca de un 50% y con menor latencia de cola
-
Respuesta JSON totalmente dinámica
| Motor |
req/s |
p99 |
| zeroserve eBPF 10 ms |
46,945 |
4.5 ms |
nginx Lua content_by_lua |
41,231 |
6.4 ms |
| zeroserve eBPF 2 ms por defecto |
32,393 |
6.7 ms |
- Con el ajuste de 10 ms, eBPF supera en throughput al
content_by_lua de nginx incluso en respuestas completamente sintéticas
- Ambos motores se compilan a código nativo; LuaJIT usa tracing JIT y async-ebpf compila JIT eBPF mediante uBPF
- En un escenario donde el cifrado TLS es el costo común por solicitud, la ruta eBPF ajustada queda por delante en throughput
- Con el valor predeterminado de 2 ms, eBPF mantiene ventaja en middleware pero pierde el liderazgo en respuestas sintéticas, por lo que se recomienda usar 10 ms en scripts de producción
Uso como reverse proxy
- zeroserve hace proxy hacia un backend cuando el script llama a
zs_reverse_proxy("http://127.0.0.1:9000")
- El pool de conexiones upstream soporta hasta 128 conexiones por backend y 30 segundos de reutilización en idle
- Para una comparación justa, nginx usó explícitamente
keepalive 128, proxy_http_version 1.1 y un encabezado Connection vacío, considerando que por defecto tiende a cerrar la conexión upstream en cada solicitud
- Caddy reutilizó conexiones con su comportamiento por defecto
- Cada proxy terminaba TLS en un solo núcleo y reenviaba a un backend compartido en texto plano; el backend corría en un servidor aparte de 2 núcleos sosteniendo sus propios 100k req/s, de modo que se midiera solo el overhead del proxy
-
Proxy de respuesta pequeña de 174 B
| Proxy |
req/s |
p50 |
p99 |
| zeroserve |
26,486 |
3.3 ms |
8 ms |
| nginx |
21,761 |
4.2 ms |
10.5 ms |
| Caddy |
7,683 |
10.3 ms |
33 ms |
- El proxy con
io_uring y pooling de zeroserve superó a nginx por cerca de un 22% y registró unas 3.4 veces más throughput que Caddy
- En cargas típicas de proxy como llamadas API, JSON pequeños o HTML desde un app server, zeroserve termina TLS y reenvía al backend más rápido
-
Proxy de respuesta de 100 KB
| Proxy |
req/s |
Throughput |
| nginx |
5,882 |
585 MB/s |
| Caddy |
4,285 |
406 MB/s |
| zeroserve |
3,631 |
359 MB/s |
- Cuando el cuerpo de la respuesta proxy crece, el buffering de nginx mueve los bytes con mayor eficiencia y toma la delantera, seguido por Caddy y luego zeroserve
- Si las respuestas proxy son grandes, nginx es una mejor herramienta; si son muchas y pequeñas, zeroserve es más rápido
Memoria
- Una sola instancia de zeroserve en idle usa cerca de 15 MB de PSS, más que los ~6 MB de nginx pero menos que los ~60 MB de Caddy
- Es importante que la unidad de ejecución sea el proceso completo; al correr una copia por núcleo, se mapea el mismo binario y se comparten las páginas de código
- Los procesos adicionales agregan poca memoria más allá de su propio working set
Publicación
- zeroserve es un proyecto open source publicado en GitHub
1 comentarios
Comentarios de Hacker News
Con la desaparición de los benchmarks de servidores web de TechEmpower, parece que estos proyectos nuevos tienen menos oportunidades de demostrar su valor
Edición: creo que estaba desactualizado, y ahora lo que está sonando es https://www.http-arena.com/leaderboard/. Suerte
Aunque tampoco es que antes se ejecutara tan seguido; viendo el historial de rondas, se corre menos de una vez al año
Me gusta ver que intentos como este se puedan explorar de forma relativamente barata y rápida gracias a los LLM
Pero lo que me deja esto es que nginx en sí mismo es bastante impresionante. Otra parte que me llamó la atención fue la explicación de que este proyecto es una alternativa a nginx y Caddy, y que apuesta por el modo de configuración
nginx y Caddy ofrecen lenguajes de configuración declarativos, y cuando llegas a sus límites, la estructura es adjuntar al lado un runtime de scripting como Lua o plugins de Caddy, así que el comportamiento queda dividido en dos capas
Pero creo que esa apuesta es equivocada. Desde hace mucho la gente prefiere configuración en vez de código, y muchas veces las funciones integradas bastan, así que no hace falta escribir código C
Todos los formatos de archivo de configuración parecen empezar simples. Incluso YAML era bastante razonable en lo básico, pero la gente empezó a querer cosas más complejas con anchors y aliases
Hasta GitLab tiene su propio formato con algo parecido a condicionales y variables, y es casi un hack que solo funciona en ciertos lugares. Apache también siguió un camino parecido con su formato de configuración basado en XML
Al final terminan apareciendo muchos lenguajes de programación a medida para gestionar la configuración. En entornos corporativos, ni siquiera se edita directamente, sino que se escriben workflows de Ansible como scripts para hacer cirugía remota
Si simplemente se hubiera integrado en el servidor un intérprete como Lua o Python para manejar la configuración, nos habríamos saltado todo ese proceso, y habría sido más simple que andar modificando archivos de configuración a medida con programas
Claro, se puede decir que esos intentos a medida están más optimizados para su uso específico que un lenguaje general, pero ese argumento solo encaja dentro del alcance estrecho de ejemplos de juguete donde ese mecanismo ni siquiera habría hecho falta desde el principio
¿Se acuerdan de los archivos INI de Windows? Eran buenos tiempos, cuando el código era código y los datos eran datos
Más simple todavía: podría leer todos los manifiestos de Ingress de un clúster de Kubernetes y volver a generar el pack
El punto es que la interfaz entre herramientas y configuración también es solo otra API, y los operadores del sistema ya describen el estado del sistema con construcciones de nivel más alto; los bytes concretos que forman la configuración son solo el resultado
Desde la perspectiva de la IA, ese enfoque podría ser más fácil de manejar. Como la IA puede trabajar con ambos lados, puede que pase bastante tiempo antes de que ese cambio se consolide claramente como una buena idea
La idea me gusta
Pero me daría más confianza si en el directorio eBPF se pudieran poner archivos
.rsen lugar de.c. Ya es un proyecto en Rust, ademásY por alguna razón yo esperaba un servidor web acelerado por kernel. Si eso se pudiera hacer de forma segura con eBPF, sería realmente impresionante
¿Y además monohilo? En Linux, hacer fork y compartir la cola de conexiones entrantes es casi trivial, y en Rust deberían ser unas pocas líneas. Con SO_REUSEPORT, el kernel se encarga del resto
Por cierto, si la idea es empujar io_uring, yo diría que también hay que empujar kTLS. Si se puede evitar el procesamiento SSL en espacio de usuario después del handshake, el diseño se simplifica muchísimo
Hasta ahora había estado usando nftables para este tipo de cosas, así que no lo había necesitado directamente
Muy genial. Me pregunto si esto se podría combinar con otros tipos de programas BPF, como un programa XDP o programas conectados a mapas de sockets, para integrar funciones HTTP de L7 en capas más bajas
La idea está bien, pero no sé si convenga enfocarse en archivos estáticos. Hoy en día rara vez se levanta un servidor nuevo para ese propósito
Así que esto se siente como hecho para mí, aunque admito que probablemente no soy un usuario típico
Se ve bien y las funciones también parecen decentes. Pero se siente demasiado artificial en algún sentido, así que no termina de convencerme.
No hay forma de saber si las métricas son falsas, si las funciones auxiliares realmente funcionan o si se hizo un trabajo serio de hardening.
Puedo aceptar que lo hayan hecho con vibe coding y que hasta el README se haya generado automáticamente. Pero incluso la entrada del blog de lanzamiento fue hecha por IA, y no tengo ninguna base para juzgar si su idea de calidad de software se parece a la mía.
Qué mundo tan raro. Hace unos años, si esto se hubiera anunciado sin avisar del uso de IA, lo habría aceptado sin sospechar. Ahora, en cuanto veo un README elegante y parámetros de línea de comandos que suenan plausibles, de inmediato sospecho que el README alucinó y que en realidad quizá ni existan esas opciones.
Cuando construyo zeroserve sí uso bastante ayuda de IA, pero verifico personalmente la salida de la IA y yo asumo la responsabilidad.
En un solo núcleo, zeroserve sirve archivos pequeños alrededor de un 17% más rápido que nginx y con menor latencia de cola. Ese es el caso donde zeroserve acierta: páginas HTML, JSON pequeño y CSS.
Con un archivo estático grande de 100 KB, zeroserve da 8,000 req/s, 782 MB/s y p99 de 22 ms; nginx 7,600 req/s, 773 MB/s y p99 de 28 ms; y Caddy 6,084 req/s, 590 MB/s y p99 de 44 ms.
Aun así, yo elegiría un proyecto antiguo auditado, probado en producción y endurecido, antes que un proyecto nuevo como este. La mejora no es tan grande como para justificar el riesgo.
Decidí quedarme en la era antigua tanto como sea posible. Gente inteligente publica software y gente inteligente lo mantiene. Ellos no necesitan IA. Ese es mi nicho.
Puede que desaparezcamos, pero aun así prefiero eso. Claro, con la condición de que esa gente inteligente escriba documentación. También hay mucha gente inteligente que odia escribirla.
Hace mucho decidí que el software sin documentación, por excelente que sea, no vale mi tiempo. Hablo más que nada de aplicaciones; casi nunca miré la documentación de Linux, aunque otros dicen que no está tan mal, así que no sé.
Es un concepto nuevo interesante y me gusta.
La verdadera pregunta es el nivel de compromiso del desarrollador y la comunidad. La gente de Caddy y Nginx ha dado soporte constante a sus productos, y este proyecto también va a requerir mucha concentración y atención.
¿Por qué tarball?
No extrae nada al disco. Como el sitio completo está contenido en ese único archivo, no hay un document root que una regla de location mal hecha pueda exponer, y el despliegue se vuelve un único reemplazo atómico de archivo.
Aunque esa explicación también podría ser una justificación al estilo LLM. Por todo el texto aparecen expresiones como “the right shape” o “the surface is broad”.