Guía9 min

ulimit nofile para un agente IA: EMFILE no es el modelo

Resumen

Cómo no quedarte sin file descriptors: ulimits nofile en Compose, sockets a Telegram y a la API, sqlite y logs. Distinto de pids_limit. Un leak de sockets parece un bot mudo. Techos oficiales Docker del 3 de septiembre de 2026.

Docker
Tope de file descriptors frente a sockets abiertos de un agente

Qué resuelve

Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.

EMFILE: too many open files tumba fetch al modelo y el webhook. No es un 429. Es el tope de FDs. pids cuenta procesos; nofile cuenta sockets y archivos. Fuentes oficiales consultadas el 3 de septiembre de 2026.

La regla

Soft/hard nofile explícito (p. ej. 1024 o 4096). Default del contenedor a veces es 1024 y un leak de http.Agent lo come. Mide /proc/sys/fs/file-nr no es el cgroup; mira ls /proc/self/fd | wc dentro.

Compose:

ulimits:
  nofile:
    soft: 1024
    hard: 2048

No pongas 1 millón “por si el agente escala”. Eso es el host.

Qué abre un turno

stdin/out/err, sqlite, un socket a Telegram, N sockets al proveedor, a veces DNS. Keep-alive mal cerrado = leak. http.globalAgent con maxSockets es el semáforo, no subir nofile a 1e6.

Logs a archivo sin rotar (disco) también son FDs.

Fly / Railway / Workers

Esta guía es Compose/VPS. Workers no tiene ulimit de Linux. Vercel Functions: el runtime pone techos. No copies el yaml a wrangler.

Tabla

nofile versus pids y RAM

TopeUnidadSíntoma al pasarlo
nofileFDsEMFILE, bot mudo
pidsprocesosEAGAIN fork
membytesOOM kill
nproc hostprocesos hostload absurdo

Errores comunes

Sockets huérfanos llenando nofile

SíntomaCausaFix
EMFILEleak Agent / no closemaxSockets + tope
1e6 nofilecopy-paste1024–4096
“Telegram timeout”FDs, no la APIls fd
sqlite locked + EMFILEmuchos db handlesuna conexión
log file /turnono rotarstdout

Relación con el resto

Checklist

  • ulimits.nofile en Compose
  • Número razonable, no 1e6
  • http.Agent con tope
  • sqlite un handle
  • Ensayo: ls /proc/1/fd | wc en un turno
  • Alerta si fd count sube entre turnos idle

FAQ

¿ulimit -n en entrypoint? Docker lo pisa o no. Usa el campo Compose.

¿soft vs hard? El proceso puede subir soft hasta hard. Fija ambos.

¿K8s nofile? Security context / ulimits del container.

Cómo listar FDs

ls -l /proc/1/fd (o el pid de node). Sockets vs archivos vs anon. Si ves cientos de TCP a api.openai.com idle, el Agent no recicla.

lsof -p PID en el host si el contenedor no tiene lsof. No instales lsof en el runner; usa Preview o docker exec con una imagen debug.

TIME_WAIT no es leak eterno; es el kernel. Cientos de ESTABLISHED sí.

El ensayo: carga 50 requests; fd count vuelve al baseline. Si no, leak.

Un keep-alive eterno a OpenAI no es “performance”. Es 50 FDs por nada. Cierra o pon timeout.

1024 basta para un bot de un worker. Si mides 800 idle, hay leak, no “necesito 10k”.

Compose sin ulimits hereda el daemon. En un VPS recién instalado el default puede ser 1024 o 1048576. Escríbelo. Si heredas 1048576, el leak no falla: se come el host. El tope bajo es una alarma.

--ulimit no es un cgroup

docker container run (hoy): el rlimit del contenedor no se sube desde dentro con privilegios default. Por eso existe --ulimit. Formato soft:hard. Ejemplo oficial: --ulimit nofile=1024:1024 y ulimit -n dentro imprime 1024. Si omites el hard, Docker copia el soft a ambos. Si omites el flag, hereda el daemon.

nofile = RLIMIT_NOFILE (descriptores abiertos). nproc = RLIMIT_NPROC (procesos). as (address space) está deprecado: no lo copies de un gist viejo.

nproc no es por contenedor

La doc avisa: Linux aplica nproc al usuario, no al contenedor. El ejemplo oficial arranca cuatro contenedores con -u daemon --ulimit nproc=3; el cuarto top falla aunque cada caja “tenga su yaml”. No uses nproc como si fuera pids. El tope de forks es el cgroup.

Compose: entero o mapa

Compose ulimits: un entero (un solo techo) o mapa soft / hard. Ejemplo oficial: nproc: 65535 y nofile.soft: 20000 / hard: 40000. Esos 20k/40k son el ejemplo de la spec, no una receta de bot. Un webhook con leak a 20k FDs tumba el host antes que el cgroup. Para un worker: 1024–4096 escritos, y http.Agent con maxSockets.

ulimit -n en el entrypoint a menudo no basta: el proceso ya nació con el rlimit del runtime. Usa --ulimit o el campo Compose.

Un keep-alive a la API sin tope no es “performance”. Es un FD por conexión idle. El techo bajo hace visible el leak en Preview, no en prod a las 4 AM.

Qué no cubre nofile

No cuenta procesos: eso es pids. No corta RSS: eso es OOM. Un rootfs read-only sigue abriendo sockets. cap_drop ALL no cierra FDs.

Workers y Vercel Functions no tienen este rlimit de Linux. No copies el yaml a wrangler.

El ensayo: 50 requests; ls /proc/1/fd | wc vuelve al baseline. Si no, leak. 1024 basta para un worker. Si mides 800 idle, no “necesito 10k”: cierra handles.

--ulimit nofile=1024 sin hard: Docker pone 1024:1024. --ulimit nofile= vacío hereda el daemon: en un VPS puede ser un millón y el leak no pega EMFILE, pega el host. Escríbelo.

Siguiente paso: si nofile ya está y igual EMFILE, logs y busca handles. Sin runtime: curso.