Se você já escreveu um script, você sabe como ele se comporta. O arquivo fica parado até você chamá-lo. Você abre um terminal, digita ruby importa.rb, e o código é executado em ordem, de cima pra baixo, como você o escreveu. Se você quiser que uma função rode, você escreve a chamada dela. Se você não chamar, ela não roda. O controle é todo seu, e o programa é encerrado quando a última linha termina de rodar.
Mas nenhum projeto web funciona assim, e a diferença é muito maior do que parece. Você prepara um servidor, digita a URL no navegador, e alguma coisa mágica acontece.
A página HTML que você criou aparece, o método do seu controller foi chamado, mas ao mesmo tempo não há uma linha no seu projeto que indique onde isso é chamado. Você não chamou nada. Alguém chamou por você.
O log do seu projeto registra esse ‘duende mágico’ trabalhando. Um app Rails sendo chamado exibe no console algo assim, várias vezes por segundo:
I, [2026-07-18T14:22:07.918234 #4821] INFO -- : [a3f1c9d2] Started GET "/restaurantes/cantina-do-ze" for 187.45.22.10 at 2026-07-18 14:22:07 -0300
I, [2026-07-18T14:22:07.919871 #4821] INFO -- : [a3f1c9d2] Processing by RestaurantesController#show as HTML
I, [2026-07-18T14:22:07.920144 #4821] INFO -- : [a3f1c9d2] Parameters: {"slug"=>"cantina-do-ze"}
I, [2026-07-18T14:22:07.938902 #4821] INFO -- : [a3f1c9d2] Completed 200 OK in 19ms (Views: 12.4ms | ActiveRecord: 3.1ms | Allocations: 8241)
Qualquer dev que use terminal passa por cima disso quinze vezes por dia lendo só o 200 e o tempo no fim.
Você pode até achar isso inútil e se perguntar: dessas coisas todas, quantas meu código chamou? Por que tanto detalhe para uma simples abertura de página show?
A resposta é uma. Enquanto essa requisição acontecia, o que saiu do seu teclado foi um Restaurante.find_by! em algum lugar. Todo o resto aconteceu sozinho, porque o framework já tinha decidido a ordem, escolhido o método e montado a resposta sem te perguntar. É o que a gente chama de convenção: o Rails assume um monte de coisa por padrão pra você não precisar dizer nada.
A mesma facilidade que te deixa criar um CRUD completo em minutos cobra um preço. O framework te entrega uma quantidade absurda de comportamento e de convenção oculta, e não é porque você não vê uma coisa que ela deixa de existir. Durante todo o processo de servir seu projeto há o núcleo do Rails no comando e você, sentado no lugar do passageiro apenas admirando a paisagem. Fácil, simples e até um pouco relaxante.
Mas quando algo dá errado numa parte que você não escreveu, o stack trace exibe um monte de arquivo que você nunca abriu, e o encanto vira desespero. Ninguém te mostrou esse trajeto, você está perdido no meio dele e não sabe nem por onde começar a corrigir.
Isso não é uma particularidade do Rails. Se você já escreveu app.get('/rota', handler) no Express, olhe de novo pra essa linha. Você registrou o handler e nunca chamou o handler.
Esse problema é muito comum e, conversando com alguns alunos aqui da Academia do Ruby, percebi que eu precisava deixar isso mais claro. Considere esse conteúdo um guia rápido de como qualquer aplicação web funciona. Como o nosso terreno aqui é Rails, os exemplos vêm dele e a gente entra nas especificidades do framework. Mas você vai encontrar paralelo direto com Laravel, NestJS e até com o Sinatra.
Eu gosto de imaginar uma requisição como uma nave descendo até o centro de um planeta, vindo do hiperespaço. Ela atravessa camada por camada, cada uma com uma função, chega no núcleo e volta pela mesma rota. Essa nave é autônoma e você não a pilota. O trajeto foi definido antes da decolagem, quando alguém apertou um botão ou digitou uma URL, e é isso que a gente vai conferir aqui camada por camada.
No fim eu volto pra essas mesmas quatro linhas de log, e os pedaços que importam vão ter dono.
O Espaço Profundo: antes do Rails ser acionado
Essa é a primeira camada, e sua aplicação ainda não sabe de nada. Ela nem foi acordada.
O navegador pega o domínio e pergunta pra rede quem é. Resolve o DNS para traçar a rota, abre a conexão, negocia o TLS. Só então ele escreve o pedido, que é texto puro:
GET /restaurantes/cantina-do-ze HTTP/1.1
Host: app.meudelivery.com.br
Cookie: _delivery_session=...
É isso que sai da máquina do usuário. Nada de objeto, nada de código Ruby, apenas umas linhas de texto seguindo um formato dos anos 90.
A aproximação e a órbita
A camada seguinte é a órbita, em produção ela existe na forma de um proxy na frente da sua aplicação, uma CDN, ou os dois. Rodando na sua máquina ela não existe e o navegador fala direto com o Puma. Ela age como um escudo em órbita, decide se o pedido merece seguir viagem, e anota de onde ele veio num cabeçalho tipo X-Forwarded-For. Se fosse código, seria mais ou menos isso:
se a resposta já está em cache -> devolve e encerra
senão -> encaminha pro servidor de aplicação, anotando o IP de origem
Uma consequência prática disso: qualquer bloqueio que acontece aqui não gera linha nenhuma no seu log de aplicação, porque seu servidor nunca soube que existiu essa requisição. Alguém processou esse bloqueio, o proxy ou a CDN, mas a sua aplicação ficou descansando.
Se você utiliza Cloudflare, com nuvem laranjinha, em seus projetos ele faz esse trabalho. Até aqui, zero código seu.
Atmosfera: você foi puxado
Sua requisição chegou. Agora alguém precisa transformá-la em input para o código Ruby e acionar sua aplicação.
Esse alguém é o servidor de aplicação. Puma, na maioria dos projetos Rails modernos, mas podia ser o Passenger, o Unicorn ou até o Falcon. O que interessa aqui é que eles fazem a mesma coisa, e é por isso que eu não vou entrar em configuração de nenhum específico.
O processo é ler aquele texto e montar um Hash para passar os dados pra frente:
env = {
"REQUEST_METHOD" => "GET",
"PATH_INFO" => "/restaurantes/cantina-do-ze",
"HTTP_HOST" => "app.meudelivery.com.br",
"rack.input" => um objeto de IO com o corpo da requisição,
# e mais algumas dezenas de chaves
}
Esse Hash tem nome, env, e é o contrato do Rack que funciona como base para qualquer projeto Ruby web, mesmo sem ser Rails. Todo servidor compatível com o Rack entrega isso, e toda aplicação Rack sabe receber isso.
E aí vem a linha que resume o artigo inteiro:
status, headers, body = app.call(env)
Leia devagar. app é a sua aplicação. call é o método que o Rails expõe. env é o Hash que o servidor acabou de montar.
Você lembra do script do começo, aquele que só rodava quando você chamava? Aqui é o contrário. O processo fica de pé indefinidamente, aguardando alguém falar com ele, e quando alguém fala é ele que chama o seu código.
Podemos dizer que aqui a relação é invertida e o servidor chama você.
Existe um nome bonito pra esse tipo de coisa: inversão de controle. Agora que você viu a linha, o nome fica fácil de entender: quem segura o call não é você, é o servidor de aplicação. Se você já se perguntou onde fica o main de uma aplicação Rails, é aqui, e você não precisou escrevê-lo pra ele existir.
A nave passou da atmosfera. Daqui em diante tudo é código Ruby, e quase nada é seu.
Crosta: a pilha de middlewares
Sua aplicação recebeu o env. Antes de chegar em qualquer controller, esse Hash atravessa uma pilha de camadas empilhadas uma sobre a outra.
Você pode ver a sua agora rodando um simples comando:
bin/rails middleware
Sai uma lista com vinte e tantas linhas. Rack::Sendfile, ActionDispatch::Static, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, Rack::MethodOverride, e por aí vai, terminando no roteador que você declarou no config/routes.rb.
Cada uma é um objeto que responde a call. Quando a aplicação sobe, o Rails monta essa lista de trás pra frente e entrega pra cada camada uma referência à camada seguinte, que fica guardada num @app. Então o @app não é a aplicação inteira, é só quem vem depois na fila. O formato é sempre o mesmo:
def call(env)
# descida: a requisição está entrando
status, headers, body = @app.call(env)
# subida: a resposta está voltando
[status, headers, body]
end
Olhe onde está o @app.call(env). No meio do método. Tudo que você escrever acima dessa linha roda na entrada, tudo abaixo roda na saída com a resposta já pronta na mão.
É por isso que o percurso é de ida e volta pela mesma escada. Aqui dentro isso é literal: não existe caminho de retorno separado, cada camada ficou parada esperando a de baixo terminar, e quando o resultado sobe ela retoma o controle exatamente no ponto em que parou. Da atmosfera pra cima a coisa é mais frouxa, porque DNS e TLS não são desfeitos na volta, a conexão já está aberta e a resposta só desce por ela.
Aquele session[:user_id] que você lê no controller? Uma camada daqui deixou a sessão disponível pra você na descida. Se você mexer nela, a mesma camada escreve um Set-Cookie na subida.
E repare no poder que cada uma tem. Se um middleware resolver não chamar @app.call, a requisição para ali e volta sem nunca ter visto o seu controller.
Manto: o roteador escolhe o método
No fim da pilha está o roteador. Ele olha o verbo e o caminho e decide quem atende.
Você declarou isso:
get "/restaurantes/:slug", to: "restaurantes#show"
Parece configuração, mas get é um método, e essa linha executa quando a aplicação sobe. Ela guarda num lugar a informação de que esse caminho pertence àquela action. Depois, com a requisição na mão, o Rails consulta o que você guardou e faz mais ou menos isto:
RestaurantesController.action(:show).call(env)
Você escreveu restaurantes#show como texto num arquivo de rotas. O Rails transformou esse texto numa chamada de método.
Faz um teste no seu projeto agora. Abre o controller, olha o def show, e procura no resto do código quem chama esse método. Você não vai achar. Nem você, nem uma gem, nem um arquivo seu. O nome existe duas vezes: na definição e dentro de uma string no arquivo de rotas.
O :slug também é resolvido aqui. O roteador extrai o pedaço do caminho e joga no monte que vai virar params no seu controller, junto com o que vier de query string e de corpo de formulário. Mais uma coisa que aparece lá sem você ter atribuído nada.
Camada interna: o que você registrou meses atrás
Antes da action rodar, roda o que você registrou.
class RestaurantesController < ApplicationController
before_action :definir_cidade_atual
rate_limit to: 30, within: 1.minute
def show
end
end
Olhe essas duas linhas com atenção, porque elas escondem a melhor cena do artigo.
before_action e rate_limit são métodos, e as duas linhas executam uma vez só, quando o Ruby lê o arquivo e monta a classe. Nenhuma requisição existe ainda. O que essas chamadas fazem é anotar numa lista que, quando um dia essa classe atender alguém, tem coisa pra rodar antes.
Então tem dois momentos, e eles nem são no mesmo dia. Você chama o Rails uma vez, no boot, pra registrar. O Rails chama você depois, em toda requisição, pra executar. Se você quiser um resumo de inversão de controle que caiba numa frase, é esse.
É aqui que mora o rate_limit nativo do Rails 8. Ele é um before_action como qualquer outro, tanto que registra usando o próprio before_action por baixo. O que quer dizer que ele roda depois que todo o resto do caminho já foi pago: a conexão foi aceita, o Puma tirou uma thread do bolso pra atender, o env foi montado, a pilha inteira de middleware foi atravessada.
Só então ele conta e decide se barra.
Compare com o bloqueio lá da órbita, que não custou nada pra sua aplicação. É o mesmo resultado pra quem está do outro lado e um custo completamente diferente pra você. Se essa página de restaurante for pública e alguém resolver raspar seu catálogo inteiro, a diferença entre barrar na órbita e barrar no manto aparece na conta do fim do mês.
Detalhe que pega muita gente: se um before_action responde alguma coisa, com redirect_to ou head, a cadeia para e a action nunca roda. Aquele Processing by RestaurantesController#show do log não é promessa de que o def show executou.
Núcleo: a action
Chegamos no centro do planeta.
def show
@restaurante = Restaurante.find_by!(slug: params[:slug])
end
Duas linhas. Esse é o único trecho de todo o percurso em que você é quem chama alguma coisa.
Vale olhar o tamanho da coisa. O caminho até aqui teve DNS, TLS, proxy, servidor de aplicação, vinte e tantos middlewares, roteamento, callbacks. E a sua contribuição é uma consulta e uma atribuição de variável de instância.
Isso é o negócio funcionando como foi desenhado. Rails é grande justamente pra que a sua parte seja pequena. O problema começa quando alguém acha que a sua parte é o todo, porque aí qualquer coisa que quebre fora dela vira mágica quebrada.
Repare também no que a action não faz. Ela não renderiza nada e não escreve na resposta. Como todo método Ruby ela devolve alguma coisa, no caso o restaurante que acabou de ser atribuído, só que o Rails ignora esse retorno. O que ele leva é o estado que você deixou pra trás: a variável de instância. Terminou o método, acabou o seu turno.
A subida
O def show retornou e ninguém pediu template nenhum.
O Rails assume. Não houve render explícito, então ele procura app/views/restaurantes/show.html.erb, renderiza dentro do layout e transforma isso numa string. As variáveis de instância que você criou na action ficam visíveis na view por causa dessa mesma etapa.
O resultado vira uma tripla:
[200, { "Content-Type" => "text/html; charset=utf-8" }, ["<!DOCTYPE html>..."]]
Status, cabeçalhos, corpo. Exatamente o formato que o Rack espera, e exatamente o que aquele app.call(env) lá da atmosfera estava esperando desde o começo.
Agora essa tripla sobe, pelas mesmas camadas da crosta, na ordem inversa, cada uma retomando no ponto em que tinha parado.
Se a sessão mudou durante a requisição, a camada de sessão escreve o Set-Cookie nos cabeçalhos. A de compressão pode reescrever o corpo. Cada uma tem sua chance de mexer na resposta na subida, e nenhuma delas foi chamada por você.
No topo, o servidor de aplicação recebe a tripla de volta, escreve o texto no socket e fecha o ciclo. O que sai é tão texto quanto o que entrou:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: _delivery_session=...
Sete camadas na descida, sete na subida, uma action no meio.
A linha lida de novo
Volta lá no começo e lê o log outra vez. E já começa por uma coisa que eu não disse antes: nenhuma dessas linhas foi escrita pelas peças que estavam trabalhando. O roteador não narra, a action não narra. Quem escreve o log é um middleware lá na crosta e a instrumentação do Action Controller. Você está lendo o relato de dois observadores, não a fala dos protagonistas.
Started GET "/restaurantes/cantina-do-ze" sai de um middleware da crosta, e sai na descida, antes de ele chamar o @app dele. Quando essa linha aparece, a requisição ainda tem meia pilha pela frente e não passou nem perto do roteador.
for 187.45.22.10 é o que o Rails entendeu como IP de origem, montado a partir dos cabeçalhos que a órbita escreveu e da lista de proxies que você marcou como confiáveis.
[a3f1c9d2] é o request id. Um middleware garante que ele exista, aproveitando o que veio de fora se veio, e é ele que te deixa juntar as quatro linhas quando o log tem trinta requisições concorrentes misturadas.
Processing by RestaurantesController#show já vem do outro observador, a instrumentação do controller. Ela conta o resultado do manto, o roteador tendo resolvido o texto restaurantes#show numa chamada de método.
Parameters: {"slug"=>"cantina-do-ze"} é o :slug da rota depois de virar params.
Completed 200 OK in 19ms também é a instrumentação do controller, fechando a conta da parte dela. Repare no efeito colateral disso: quando essa linha é escrita, a tripla ainda vai subir a crosta inteira antes de virar texto no socket.
Views: 12.4ms | ActiveRecord: 3.1ms é a distribuição do tempo entre a renderização e a sua consulta. Olha a proporção e o que ela diz sobre o tamanho relativo da sua parte.
Nada disso é mágico. É um Hash descendo a escada, um método sendo chamado no fundo, e uma tripla voltando escada acima.
E agora a pergunta que abre o próximo artigo. Se o rate_limit do Rails 8 roda como before_action, ele mora na camada interna, o que quer dizer que quem está te atacando já pagou a órbita, a atmosfera, a crosta e o manto antes de ser barrado.
Isso é bom o suficiente? Depende de quem está batendo na porta e de quantos por segundo.
Até a Próxima!
Ainda não há comentários. Seja o primeiro a comentar!