· 24 min de lectura · Por Grover Cordova

Recolector de basura vs. Rust: pausas, picos y latencia

Cómo funciona un recolector de basura, por qué puede causar picos de latencia y qué cambia con Rust. Con diagramas, un simulador y sus límites.

Todo programa debe devolver la memoria que ya no usa. La pregunta no es si cuesta, sino quién paga, cuánto y en qué momento. Esa respuesta define cómo se comporta tu sistema bajo carga.

Esta nota no busca declarar un ganador. Para muchísimos sistemas, un recolector moderno es la decisión correcta. Pero si tu negocio depende de responder siempre a tiempo, conviene entender dónde nacen los picos.

Tres formas de devolver la memoria

Cen usoRecolectoren usoRusten usofree(): depende de tien esperalibera} libera aquí, siempretiempo
Las tres liberan memoria. Cambia quién decide y cuándo ocurre.
  • Manual, como en C: tú decides cuándo liberar. Es rápido y predecible, pero un error produce estos fallos.
  • Recolector de basura, como en Java, Go o C#: un proceso busca lo que ya no se usa y lo recupera más tarde. Es seguro y cómodo.
  • Propiedad, como en Rust: el compilador decide dónde liberar, y lo escribe en el programa. El Libro de Rust lo resume así: al salir de alcance, el valor se libera.

Cómo trabaja un recolector de basura

La idea central es la alcanzabilidad: si ningún camino lleva a un objeto, nadie podrá volver a usarlo.

RaícesABCDEAlcanzables:se conservanSin camino: basura
Marcar: recorrer todo lo que se alcanza desde las raíces. Barrer: recuperar el resto.

Los recolectores actuales son mucho más sofisticados que este esquema. Trabajan en paralelo a tu programa, dividen los objetos por edad y compactan la memoria. Python combina otra técnica, el conteo de referencias, con un recolector para los ciclos.

El costo no es solo la pausa

Se suele hablar de las pausas, pero son solo uno de tres costos:

  1. Memoria. El recolector necesita espacio para trabajar. En Go, duplicar GOGC duplica la memoria adicional y reduce a la mitad el costo de CPU: es un intercambio explícito. Con el valor habitual de 100, el montón puede crecer hasta cerca del doble de los datos vivos.
  2. CPU. Marcar objetos es trabajo real. La misma guía de Go indica que el recolector usa el 25 % de los recursos de CPU durante la fase de marcado.
  3. Variación. El trabajo llega en momentos que tu código no eligió, y compite con las peticiones en curso.
Con recolector de basuradatos vivos: línea punteadaCon Rustmisma escala
Un recolector necesita holgura para trabajar. Ilustración conceptual, no medición.

Por qué aparecen los picos

Go hace casi todo su trabajo de forma concurrente, y sus pausas completas son breves. Aun así, su guía enumera varias fuentes de latencia: las pausas breves entre fases, los retrasos de planificación por el 25 % de CPU, las rutinas que deben ayudar al recolector cuando se asigna mucha memoria y el costo extra de escribir punteros durante el marcado.

Con recolector de basuracada pico: el recolector compiteCon Rustmisma escalatiempo de respuesta a lo largo del tiempo
Ilustración conceptual, no medición. El tamaño de los picos depende del recolector, del montón y de la carga.

El tiempo de un ciclo crece con los datos vivos y con la cantidad de punteros por recorrer. Por eso las cachés grandes en memoria son el caso difícil: cuanto más útil es la caché, más trabajo le das al recolector.

Qué le pasa al flujo del sistema

Aquí está lo que más se subestima: una pausa no solo retrasa a quien estaba siendo atendido. Las peticiones siguen llegando, se acumulan y hay que atenderlas después, con la capacidad que sobra.

pausa: 50 msla cola tarda 117 ms en vaciarsecolatiempo
Con un 70 % de carga, una pausa de 50 ms afecta unos 167 ms de tráfico. La cola dura más que la pausa.

Cuanto más cargado está el sistema, menos capacidad libre tiene para recuperarse. La misma pausa duele mucho más al 90 % que al 50 %.

Peticiones retenidas
50
Tiempo en vaciar la cola
117 ms
Peticiones afectadas
167
Simulador con un modelo simplificado: cola = peticiones × pausa, y se vacía con la capacidad libre. La animación va en cámara lenta. Prueba subir la carga a 90 %.

La regla es sencilla. Con una carga u, la cola tarda en vaciarse la duración de la pausa multiplicada por u / (1 − u). Una pausa de 50 ms toma 50 ms más en vaciarse al 50 % de carga, y 450 ms al 90 %. Un sistema real es más complejo, pero la tendencia se mantiene.

El promedio esconde el problema

Si mides solo el promedio, nunca verás esto. Por eso se usan percentiles: p99 es el tiempo por debajo del cual está el 99 % de las peticiones. Dicho de otro modo, 1 de cada 100 tarda más.

medianapromediop99rápidolentola cola
La mediana y el promedio cuentan la historia de la mayoría. El p99 cuenta la de quien sufre. Ilustración conceptual.

Un p99 alto no es un detalle estadístico. En un servicio con un millón de peticiones al día, son diez mil respuestas lentas diarias, y suelen concentrarse en los momentos de mayor carga, cuando más importan.

Con muchos servicios, el pico deja de ser raro

Una acción de un usuario suele tocar varios servicios, y su respuesta es tan lenta como el más lento. Jeff Dean y Luiz Barroso lo describieron en The Tail at Scale:

1 servicio1 %10 servicios9,6 %25 servicios22,2 %50 servicios39,5 %100 servicios63,4 %
Porcentaje de peticiones de usuario con al menos una respuesta lenta, si cada servicio se demora en 1 de cada 100 llamadas: 1 − 0,99n. Es aritmética, no una medición.

Lo que en un servicio parecía raro, en un sistema de cien servicios le pasa a la mayoría de las peticiones.

Un caso real: Discord

El caso más citado es el de Discord. Su servicio Read States, escrito en Go, guardaba decenas de millones de registros en una caché y mostraba picos periódicos de latencia. Discord los atribuyó al recolector: debía recorrer toda la caché, y Go fuerza una recolección al menos cada dos minutos. Reducir la caché aliviaba los picos, pero empeoraba el p99, porque más consultas iban a la base de datos.

Al reescribirlo en Rust, los picos desaparecieron. Con optimizaciones, superó a la versión anterior en latencia, CPU y memoria. Según un artículo de AWS, la versión en Rust fue más de 10 veces más rápida en general y redujo 100 veces las peores latencias.

Conviene leerlo con cuidado:

  • Se trata de un servicio concreto, con un patrón muy favorable a Rust.
  • Usaba Go 1.9, y el artículo es de 2020. Go ha seguido mejorando.
  • Los propios autores aclaran que no recomiendan reescribirlo todo en Rust.

Otro caso: Apple, de Java a Swift

El problema no es exclusivo de Go, y la solución no siempre es Rust. En 2025, Apple contó que migró de Java a Swift el servicio que revisa contraseñas filtradas. Tras ajustar el recolector G1, seguía viendo pausas prolongadas bajo carga. Con Swift reportó cerca de un 40 % más de capacidad, memoria de cientos de megabytes por instancia, frente a decenas de gigabytes en Java, y latencias menores a 1 ms en el 99,9 % de las peticiones.

Es un caso publicado por la empresa creadora de Swift, pero la lección es general: en servicios muy exigentes, una gestión de memoria determinista puede cambiar el resultado, sea con Rust o con otro lenguaje.

Cómo libera Rust

En Rust no hay un proceso aparte. El punto de liberación queda fijado al compilar:

fn main() {
    {
        let reporte = vec![0u8; 1024];
        println!("{} bytes", reporte.len());
    } // `reporte` se libera aquí, en este punto exacto

    println!("La memoria ya se devolvió");
}

Por eso el costo es predecible: ocurre siempre en el mismo lugar del código, y lo paga la operación que creó el problema, no una petición ajena. Lo explicamos en la guía de ownership.

Lo medimos nosotros

Quisimos comprobarlo. En nuestro microservicio de ejemplo, con 5 millones de registros en memoria y un solo núcleo, el p99 de Go subió a más de 80 ms. Con 4 núcleos bajó a 2,6 ms. Node.js, también con recolector, se mantuvo cerca de 2 ms con un núcleo. La conclusión es que no todos los recolectores se comportan igual, y que los recursos disponibles cambian el resultado.

Rust también puede tener picos

Sin recolector no hay magia. Estas son las fuentes habituales de picos en Rust:

  • Liberar estructuras enormes. Soltar un mapa de un millón de entradas toma tiempo, y ocurre justo en esa línea.
  • Bloquear el hilo en código asíncrono. Una operación lenta dentro de una tarea detiene a las demás que comparten ese hilo.
  • Contención en candados. Un Mutex muy disputado produce colas, en cualquier lenguaje.
  • Reservar memoria en la ruta crítica. El asignador tampoco es gratis.
  • El mundo exterior. La red, el disco y la base de datos no saben en qué lenguaje escribes.

La diferencia es que estos costos están a la vista, en tu código, y puedes moverlos de lugar:

use std::collections::HashMap;
use std::thread;

fn main() {
    let cache: HashMap<u64, String> =
        (0..1_000_000).map(|i| (i, i.to_string())).collect();

    // Liberar un millón de entradas toma tiempo.
    // Se entrega a otro hilo para no frenar la petición en curso.
    thread::spawn(move || drop(cache));

    println!("La petición sigue su curso");
}

Los recolectores modernos son muy buenos

Sería deshonesto comparar Rust con los recolectores de hace quince años. ZGC, de Java, hace su trabajo costoso de forma concurrente, sin detener los hilos de la aplicación más de un milisegundo, con pausas independientes del tamaño del montón y para montones de hasta 16 TB.

Eso cambia la discusión, pero no la elimina. Las pausas casi desaparecen, aunque el trabajo sigue existiendo: la misma documentación de ZGC advierte que demasiados hilos del recolector le quitan CPU a la aplicación, y recomienda que el sistema nunca supere el 70 % de uso de CPU.

Comparación directa

AspectoRecolector modernoRust
Quién liberaUn proceso en tiempo de ejecuciónEl compilador lo fija en el código
CuándoCuando el recolector lo decideAl salir de alcance
PausasDesde breves hasta menos de 1 ms, según el recolectorSin pausas de recolección
CPUTrabajo adicional y continuoSin ese trabajo; el costo es la liberación puntual
MemoriaNecesita holguraCerca de los datos vivos
Latencia de colaDepende de la carga y de la configuraciónMás predecible, sin estar garantizada
Productividad inicialAltaMenor, con curva de aprendizaje real
ContrataciónMercado amplioMercado más pequeño
AjusteParámetros del recolectorDiseño del código

Cuándo el recolector es la mejor opción

Si tu duda concreta es entre dos lenguajes, mira Rust vs. Go. Quédate con tu recolector si:

  • Tu sistema pasa la mayor parte del tiempo esperando a la base de datos o a la red.
  • Tus objetivos de latencia se miden en cientos de milisegundos.
  • Tu equipo domina el lenguaje, y el costo de infraestructura no es un problema.
  • No has medido el problema. Sin datos, una migración es una apuesta.

Rust empieza a tener sentido cuando el p99 o el p99.9 es un requisito del negocio, cuando manejas muchos datos en memoria o cuando el costo de CPU y memoria se nota en la factura. También reduce la huella de carbono, en la medida en que reduce servidores.

Qué medir antes de decidir

  1. Los percentiles, no el promedio. p50, p95, p99 y p99.9, por servicio y bajo carga real.
  2. El tiempo de recolección. Actívalo con GODEBUG=gctrace=1 en Go o con -Xlog:gc en Java.
  3. Si los picos coinciden. Compara los picos de latencia con los ciclos del recolector. Si no coinciden, el problema está en otra parte.
  4. La configuración. Prueba primero a ajustar el recolector y a reducir asignaciones. Suele ser mucho más barato que migrar.
  5. Un solo componente. Si decides cambiar, elige el servicio más crítico y compara. Las cinco preguntas antes de migrar ayudan a ordenar esa decisión.

Fuentes

  1. A Guide to the Go Garbage CollectorThe Go Authors
  2. runtime/proc.go: forcegcperiodThe Go Authors
  3. Why Discord is switching from Go to RustDiscord, 2020
  4. Swift at Apple: Migrating the Password Monitoring service from JavaSwift.org, 2025
  5. The Z Garbage CollectorOpenJDK
  6. Sustainability with RustAWS Open Source Blog, 2022
  7. The Tail at ScaleJeffrey Dean y Luiz André Barroso, Google, 2013
  8. What Is Ownership?The Rust Programming Language
  9. gc: Garbage Collector interfacePython Documentation

Construye mejor software.

Seguridad, eficiencia y oficio, con fuentes y en español.