Por que uma aplicação Python em larga escala precisa de mais servidores que uma equivalente em .NET? | O caso do Habitat da OpenAI

rust dev.to

Em 11 de setembro de 2026, a OpenAI publicou um artigo explicando a arquitetura de seu serviço Habitat para o público.

Isso movimentou as redes, e uma thread do X (Twitter) me chamou atenção:

O questionamento do David Fowler (renomado engenheiro da Microsoft e co-criador do ASP.NET Core e do SignalR) foi o motivador da escrita desse artigo, buscando esclarecer essa resposta comparando o Python com o .NET.

PS: a OpenAI reescreveu o Habitat em Rust, não em .NET. A comparação com .NET aqui é porque é uma linguagem que eu gosto bastante e uso nos meus projetos pessoais, além do Python. Não é porque a OpenAI tenha feito ou publicado algum benchmark nesse sentido.

O que é o serviço Habitat da OpenAI?

O Habitat é a plataforma de armazenamento online que sustenta praticamente todo produto da OpenAI: ChatGPT, API, Codex, entre outros. Rodando em Python, o Habitat chegou a se tornar o segundo maior serviço da OpenAI em consumo de CPU, sustentando picos de mais de 20 milhões de requisições por segundo, 500+ petabytes de dados, servindo mais de 1 bilhão de pessoas por semana em quase 40 regiões e assim por diante. Com um serviço desse porte, o questionamento de David Fowler no Twitter acerca do Python é muito pertinente. Então, vamos para ela.

Por que o Python necessitaria de mais servidores do que uma aplicação em .NET, por exemplo?

Por padrão, scripts em Python são interpretados pelo CPython, implementado em C. O CPython compila o código-fonte em bytecode e o executa na Python Virtual Machine (PVM) instrução por instrução. Essa execução sequencial de bytecode pela PVM está diretamente ligada a um mecanismo central do CPython: o Global Interpreter Lock (GIL). O GIL é um mutex que garante que apenas uma thread execute bytecode Python por vez, mesmo em um ambiente com múltiplos núcleos. Ele existe porque o gerenciamento de memória do CPython (o reference counting usado no garbage collector) não é thread-safe por natureza. Sem o GIL, duas threads poderiam alterar o contador de referências de um mesmo objeto simultaneamente, causando race condition e corrupção de memória.

É exatamente esse ponto que explica por que uma aplicação Python costuma precisar de mais servidores (ou mais processos/workers) do que uma aplicação equivalente em .NET.

Vale lembrar que isso pesa especialmente em cargas com uso intenso de CPU, não em toda e qualquer aplicação Python.

E aí você pode se perguntar: mas o Python consegue executar funções assíncronas, então como o GIL trava a execução a uma única thread por vez?

A resposta é que async/await não contorna o GIL, ele resolve um problema diferente. O event loop do Python continua rodando em uma única thread; o que ele faz é alternar entre tarefas nos pontos de await, aproveitando os momentos em que uma tarefa está esperando por I/O (rede, disco, banco de dados) para dar vez a outra. Ou seja, async oferece concorrência, não paralelismo real de bytecode.

Dá pra pensar no GIL como uma camareira de hotel cuidando de vários quartos (as tarefas async) sozinha. Ela passa de quarto em quarto: se um deles está com a placa de "não perturbe" pendurada na porta, equivalente a uma tarefa esperando por I/O, ela não perde tempo ali e segue para o próximo quarto que precisa de atenção. Mas o trabalho de fato, ela só consegue fazer um de cada vez: enquanto está arrumando um quarto, os outros esperam a vez, não importa quantos existam.

Agora, como o .NET funciona e como ele pode executar múltiplas tarefas CPU-bound em paralelo?

Do lado do .NET, o caminho é parecido em estrutura, mas bem diferente no resultado. O código-fonte (C#, por exemplo) passa primeiro pelo Roslyn, o compilador oficial da Microsoft, que não gera código de máquina diretamente: ele compila o código para CIL (Common Intermediate Language, também chamado de IL ou MSIL), uma linguagem intermediária, portável, que fica armazenada no assembly (o .dll ou .exe gerado).

É só quando o programa roda que entra o RyuJIT, o compilador just-in-time do CLR (Common Language Runtime). Diferente do CPython, que interpreta bytecode instrução por instrução toda vez que o código executa, o RyuJIT compila o CIL para código de máquina nativo, específico para o processador da máquina, e faz isso method a method, na primeira vez que cada método é chamado (por isso "just-in-time": o código nativo só é gerado quando é de fato necessário).

É essa diferença que explica o paralelismo real: uma vez compilado para código nativo, esse código roda diretamente no processador, sem intermediação de uma máquina virtual "travando" a execução a uma thread por vez. As threads do .NET mapeiam para threads reais do sistema operacional, e o CLR não tem nenhum mecanismo equivalente ao GIL, porque não precisa. O coletor de lixo do .NET não usa reference counting (como o CPython), e sim um GC de rastreamento (tracing garbage collector), que foi projetado desde o início para lidar com múltiplas threads acessando e alterando objetos ao mesmo tempo, sem depender de um lock global para evitar corrupção de memória. Resultado: múltiplas threads podem executar código nativo simultaneamente, em múltiplos núcleos, dentro do mesmo processo, paralelismo real para tarefas CPU-bound, algo que o CPython só consegue contornar recorrendo a múltiplos processos (mais adiante comento sobre o free-threading, a tentativa mais recente de mudar isso).

Se o Python tem essa limitação, como a OpenAI contornou isso no Habitat?

Normalmente, para contornar isso, a saída é escalar horizontalmente dentro da própria máquina: múltiplos processos (via multiprocessing, múltiplos workers do Gunicorn/uWSGI, etc.), cada um com seu próprio interpretador e seu próprio GIL. É esse custo de contornar o GIL com mais processos, em vez de aproveitar paralelismo real de threads como o .NET faz, que se traduz em mais instâncias/servidores para sustentar a mesma carga de trabalho CPU-bound.

Sobre a OpenAI em específico, a arquitetura rodava múltiplos processos Python por pod, cada um atendendo poucas requisições simultâneas, escalando horizontalmente em vez de verticalmente (daí o questionamento do Fowler de quantos servidores são necessários para aguentar uma aplicação em Python com mais de 20M de requisições por segundo). De forma resumida, para você ter uma aplicação dessa em Python, saiba que será preciso o caixa de uma OpenAI para manter uma infraestrutura caríssima que consiga suportar todas essas requisições escalando de forma horizontal (inserindo mais máquinas cada vez mais).

Hoje, como falado no artigo, a OpenAI reescreveu esse serviço em cima da linguagem Rust. E o resultado reforça exatamente o argumento técnico que construímos até aqui. Segundo a própria OpenAI, a versão em Rust é 6 vezes mais eficiente em uso de CPU e 15 vezes mais eficiente em uso de memória que a versão em Python, além de latências médias e de cauda significativamente menores.

Uma observação importante antes de fechar

O Python moderno não ficou parado em relação a essa limitação. Desde a versão 3.13, existe o free-threading (PEP 703), um build experimental que remove o GIL de fato, com suporte oficial previsto a partir da versão 3.14. Na prática, porém, ainda é um recurso em maturação: exige recompilar extensões escritas em C e depende de um ecossistema de pacotes que ainda está se adaptando, por isso não é (ainda) uma opção óbvia para um serviço do porte do Habitat.

Também vale reforçar: o GIL não foi o único obstáculo do Habitat rodando em Python. A própria OpenAI relatou lidar com outras fricções técnicas nessa escala, atrasos de agendamento no event loop, gerenciamento de connection pooling e problemas de thundering herd na camada de proxy (são muitos termos complicados que dariam outros artigos). O GIL explica a limitação estrutural de fundo que percorremos neste artigo, mas a conta final de uma escala como a do Habitat envolve mais variáveis do que só ele.


E aí, curtiu o artigo? Deixa teu comentário, quero saber se você já passou por algum gargal com GIL/paralelismo no dia a dia e como você resolveu esse perrengue.

Source: dev.to

arrow_back Back to Tutorials