pids limit para un agente IA: nada de fork bomb en el VPS
Resumen
Cómo no dejar que un tool spawnée mil node: pids_limit en Compose y docker run. Un agente con tools no es un systemd. 256 pids bastan. Distinto de mem_limit y cpus: es el número de procesos. Docs oficiales del 3 de septiembre de 2026.

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.
Un tool while true; do node & done tumba el host, no solo el contenedor, si no hay pids. CPU y OOM no cuentan procesos. Fuentes oficiales consultadas el 3 de septiembre de 2026.
La regla
pids_limit: 256 (o 128) en el servicio del agente. Node + tini + un par de children. Si necesitas 2000, el diseño está mal (shell out masivo).
docker run --pids-limit 256.
Compose
En Compose v2 a menudo deploy.resources.limits.pids o pids_limit según archivo. Mira la doc de deploy del compose file. Si tu versión ignora deploy fuera de Swarm, usa pids_limit en el servicio.
Qué cubre
Fork bomb, npm que spawnea compilers en runtime, un tool que abre 500 ffmpeg. No cubre threads de un solo proceso (eso es CPU/RAM).
tini (init) reaps zombies; pids_limit evita nacer 10k zombies.
Fly / Railway / Workers
Fly: no es el mismo flag. Un Machine no es cgroup pids de Compose idéntico. Railway: límites del plan. Workers: un isolate, no forks. Esta guía es VPS/Compose.
Tabla

| Límite | Cubre | No cubre |
|---|---|---|
| pids | número de procesos | threads |
| cpus | tiempo CPU | forks |
| mem | RSS | fork sin malloc |
| restart | crash loop | fork storm vivo |
Errores comunes

| Síntoma | Causa | Fix |
|---|---|---|
| Load 80, 1 contenedor | sin pids | 256 |
EAGAIN fork | tope bajo | sube un poco o menos spawn |
| Swarm vs compose | deploy ignorado | pids_limit |
| Workers “pids” | no aplica | skip |
| 10000 “por si acaso” | no es tope | 256 |
Relación con el resto
- CPU: límites CPU.
- RAM: OOM.
- Restart: unless-stopped.
- Caps: cap_drop.
Checklist
-
pids_limito deploy.pids escrito - Número ~128–512, no 10k
- Ensayo: un fork loop muere con EAGAIN, el host vive
- tini sigue reaping
- No aplica a Functions
- Alerta si
nproccerca del tope
FAQ
¿Hilos de libuv? No son PIDs extra. El event loop es uno.
¿ulimit -u en entrypoint? Peor que cgroup. Usa el flag de Docker.
¿K8s resources.pids en el Pod? No. El tope va en el kubelet, no en el yaml de la app.
Cómo mirar el uso
cat /sys/fs/cgroup/pids.current (cgroup v2) o ls /proc | wc. Dentro del contenedor. Si current se acerca a 256 en un turno normal, o hay leak de child_process o el tope es corto.
Un pnpm en runtime (no) spawnea node extra. El runner no corre pnpm (multi-stage).
Preview puede tener tope más bajo (64) para pillar leaks antes de prod.
El ensayo: script de fork en Preview; el host nproc no se dispara. El contenedor sí falla.
Un agente con child_process por mensaje sin tope es un pids leak. Semáforo de children, no “ilimitado porque async”.
256 no es magia. Cuenta: tini + node + 1–2 tools. Si mides 40 pids en un turno sano, 128 es holgado.
Compose override local sin pids no es prod. El archivo que despliegas es el que cuenta. Un docker compose.override.yml de laptop no debe llegar al VPS.
nproc del host incluye systemd y otros contenedores. Mide dentro. Si el host sube y el cgroup no, el leak es otro servicio.
--pids-limit no es --pid
docker container run (hoy): --pids-limit ajusta el tope de PIDs del contenedor; -1 = ilimitado. --pid es el namespace de PIDs (compartir o no el árbol), no el recuento. Copiar el flag corto es el bug.
docker update --pids-limit usa la misma semántica. Puedes bajar el techo sin recrear el contenedor. El kernel permite pids.current > pids.max si bajas el límite con procesos ya vivos; los fork() / clone() nuevos sí reciben -EAGAIN. Un agente ya inflado no se mata solo: el tope corta nacimientos.
Compose: dos llaves, un número
Compose File, pids_limit (hoy): ajusta el tope de PIDs del contenedor. -1 = ilimitado. El ejemplo oficial es pids_limit: 10. Si también pones deploy.resources.limits.pids, tienen que coincidir.
Deploy spec: limits = la plataforma impide asignar más. El ejemplo usa pids: 1 junto a cpus: '0.50' y memory: 50M. pids es un entero, no un string con sufijo.
En Compose no-Swarm, deploy.* a menudo se ignora. El campo de servicio pids_limit es el que aplica en docker compose up de un VPS. Si copias solo el bloque deploy, el bot queda sin tope.
cgroup v2: qué hay que leer
Kernel, controller pids (hoy): pids.max default “max” (sin techo). Es un hard limit. pids.current cuenta procesos del cgroup y descendientes. pids.peak es el máximo histórico. pids.events → max cuenta cuántas veces chocaste el techo.
fork() / clone() que violarían la política: -EAGAIN. No SIGKILL. El agente “se cuelga” en un spawn, no desaparece. Por eso el síntoma parece un tool mudo, no un OOM.
Mover procesos al cgroup no está bloqueado por la política: puedes tener current > max. El ensayo de bajar el tope con docker update no mata a los children ya vivos.
Kubernetes no es resources.pids
PID limiting en Kubernetes es stable desde 1.20. Los PIDs se agotan sin tocar CPU ni RAM. Algunas distros dejan /proc/sys/kernel/pid_max en 32768.
El tope por Pod no va en el spec del Pod. Es kubelet: --pod-max-pids o PodPidsLimit. “Pod-defined PID limits are not currently supported.” No hay equivalente de pids_limit en el yaml de la app.
Ejemplo oficial: nodo con 262144 PIDs y menos de 250 Pods → presupuesto 1000 PIDs/Pod. Reserva de sistema: pid=<n> en --system-reserved / --kube-reserved.
Eviction pid.available se calcula periódico y no impone el hard limit. Una fork bomb rápida igual tumba el nodo si no hay --pod-max-pids. El hard limit es el que hace fallar el fork.
Un agente en un cluster compartido no “hereda Docker”. Si el manifiesto no documenta el kubelet del nodo, asume que el tope es el de la distro, no 256.
ulimit nproc no es pids_limit
Compose documenta ulimits.nproc: 65535 en el mismo bloque que nofile. Eso es RLIMIT_NPROC, no el controller de cgroup. Un agente con nproc alto y pids_limit ausente sigue pudiendo llenar el host: el default del cgroup es max. Fija cgroup (pids_limit). El ulimit es otra capa, más fácil de pisar desde un entrypoint.
Default: ilimitado
Sin flag, no hay techo de PIDs (cgroup max; CLI -1 = unlimited). La página de resource constraints de Engine habla de memoria y CPU; no lista pids. Si solo copiaste -m y --cpus, el fork bomb sigue abierto. El flag hay que escribirlo.
--pids-limit -1 es el antipatrón explícito: documentas que no hay tope. Peor que omitirlo, porque parece deliberado.
Threads de un mismo PID: no cuentan. libuv no crea un PID por I/O. child_process.fork sí. cluster sí. Un tool ffmpeg por mensaje sí.
Zombies: tini los reap; cada zombie ocupa un PID hasta el reap. Sin init (tini), el tope se llena de cadáveres y el siguiente spawn es EAGAIN aunque “no haya trabajo”.
Workers y Vercel Functions no tienen este cgroup. No traduzcas el yaml. Si el host igual sufre con el tope puesto, CPU y mem. Sin runtime: curso.
Lecturas relacionadas
Sigue explorando Deploy y otras piezas para builders.



