A pausa que você não agendou
O seu p50 é 40 ms. O seu p99 é 900 ms. As requisições lentas não são queries mais lentas, nem outro endpoint, nem um cliente problemático. Elas estão espalhadas por tudo, e quando você as alinha com o tempo de vida do processo, elas chegam num ritmo. Alguma coisa no processo para o mundo a cada poucos segundos, e não está no seu código.
Essa coisa é uma coleta de lixo maior (major). Numa máquina aqui, com Ruby 4.0.1, um processo segurando 217.855 objetos vivos levou 2,08 ms para uma marcação e varredura completas, e 0,56 ms para uma coleta menor (minor):
retained = []
200_000.times { |i| retained << "row-#{i}" }
GC.start
t = Time.now
GC.start(full_mark: true, immediate_sweep: true)
puts "major #{((Time.now - t) * 1000).round(2)} ms"
t = Time.now
GC.start(full_mark: false, immediate_sweep: true)
puts "minor #{((Time.now - t) * 1000).round(2)} ms"
É cerca de quatro vezes o custo, para a mesma chamada com uma palavra-chave trocada. Uma nova medição em outra máquina, em 2026-09-28, deu 4,14 ms contra 0,89 ms, o mesmo formato. Aumente o conjunto vivo de duzentos mil objetos para os vários milhões que um processo Rails real carrega, e essa razão é toda a história da sua latência de cauda.
O que você já tem, e o que falta
O curso de nível Júnior "Ruby: coleta de lixo" deu os instrumentos. Você contou objetos vivos com ObjectSpace e dividiu o heap por tipo com ObjectSpace.count_objects. Você viu as pausas acontecerem com GC::Profiler, definiu variáveis de ambiente do heap e mediu quais mudavam algo, e conheceu finalizers e WeakRef. Cada um desses é um jeito de observar o coletor.
Nenhum deles diz o que o coletor está decidindo. Essa é a lacuna que este curso fecha. Observar sem um modelo produz o que engenheiros sêniores fazem de pior: ajustar por superstição. Alguém define quatro variáveis de ambiente tiradas de um post, o gráfico se mexe por motivos que não têm relação, e a configuração vira folclore que ninguém consegue tirar.
O curso de nível Sênior "Ruby: mergulho no GVL" terminou apontando para cá. Cada alocação daquele curso alimentou um coletor cujos trinta contadores você leu sem interpretar. Cada thread interrompida por timer poderia ter parado por uma coleta. A trava e o coletor são as duas coisas que param uma thread Ruby, e você só modelou uma delas.
A ideia por baixo de todo o resto
A maioria dos objetos morre jovem. Uma requisição monta um hash de parâmetros, três objetos do Active Record e quarenta strings para um template. Aí a resposta sai, e quase tudo isso vira lixo. Um punhado de coisas sobrevive: os objetos de classe, o pool de conexões, os templates compilados, os seus caches.
Um coletor que percorre todos os objetos a cada ciclo paga o preço cheio para redescobrir esse fato milhares de vezes por segundo. O Ruby não faz isso. Ele separa os objetos por idade, varre os jovens com frequência e barato e visita os velhos raramente. O próprio interpretador confirma:
GC::OPTS
# => ["USE_RGENGC", "RGENGC_ESTIMATE_OLDMALLOC",
# "GC_ENABLE_LAZY_SWEEP", "GC_COMPACTION_SUPPORTED"]
O RGENGC é o coletor geracional, compilado e ligado. Todo diagnóstico deste curso parte do que ele tenta fazer.
O que este curso cobre
A lição 2 é a maquinaria: marcações menores contra maiores, a regra das três sobrevivências que promove um objeto a velho, a marcação incremental, e a barreira de escrita e o conjunto lembrado (remembered set) que mantêm o esquema correto.
A lição 3 pergunta o que dispara uma coleta maior, porque essa é a pausa que alguém percebe. A resposta inclui um caminho de memória de que quase ninguém ouviu falar, e o GC.latest_gc_info vai nomeá-lo para você.
A lição 4 transforma o GC.stat de trinta números num diagnóstico. Você vai reduzi-lo a três razões e lê-las como um veredito. Essa lição traz o desafio de código.
A lição 5 traça a linha que decide o que você faz na segunda-feira. Um vazamento é um conjunto de referências retidas que cresce sem limite. Inchaço é memória residente que nunca volta. Os dois parecem idênticos num gráfico de memória e pedem respostas opostas.
A lição 6 cobre compactação e copy-on-write: o GC.compact medido aqui, por que o GC.auto_compact ainda vem desligado e por que um Puma com prefork compartilha páginas até o coletor sujá-las.
A lição 7 cobre alocação e ajuste, com um veredito honesto sobre os dois. As heurísticas padrão são boas. A pergunta interessante quase nunca é qual variável de ambiente definir.
Comece pela idade de um objeto. Tudo o que o coletor faz bem, e todo jeito em que ele surpreende, vem daí.
Continue com a aula 2
Coleta geracional e a write barrier
Promoção após três sobrevivências, coletas menores que pulam objetos velhos e a barreira de escrita que torna isso seguro.
Com uma conta grátis você também tem os quizzes, seu progresso e suas anotações, e a discussão.