notes
29 jul 2026 · 1 min read

Notas sobre una fuga de memoria de tres días

Un worker en segundo plano empezó a reiniciarse cada seis horas más o menos bajo carga. Nada fallaba ruidosamente —la memoria simplemente subía hasta que el gestor de procesos lo mataba y arrancaba uno nuevo. Fácil de ignorar, que es exactamente por qué tomó tres días.

La suposición equivocada

Asumí que la fuga estaba en la biblioteca de colas, porque ahí es donde apunta todo el folklore de “objeto de vida larga”. Pasé un día agregando instrumentación alrededor de los manejadores de jobs, mirando la profundidad de la cola, revisando promesas sin resolver. Nada.

La fuga real estaba en un wrapper de logging que cacheaba un formateador por id de petición, y nunca desalojaba nada:

const formatters = new Map();

function getFormatter(requestId) {
  if (!formatters.has(requestId)) {
    formatters.set(requestId, buildFormatter(requestId));
  }
  return formatters.get(requestId);
}

Cada id de petición es único. Cada entrada se queda para siempre. Un caché sin política de desalojo no es un caché —es solo una fuga con un nombre más amigable.

Lo que realmente la encontró

Snapshots de heap, tomados con una hora de diferencia bajo carga, comparados con --diff. La diferencia de tamaño retenido apuntaba directo a formatters, que había pasado por alto en revisión de código dos veces porque “parecía un caché”.

El arreglo fue un wrapper LRU de cinco líneas. La lección tomó más tiempo en aprenderse: cuando la memoria crece monótonamente, dejá de adivinar y compará dos snapshots de heap antes de tocar cualquier código.