Um bidder em Go que perde leilões por timeout enquanto sua média parece saudável tem dois problemas sob um sintoma: um deadline que pertence à exchange e cobre a ida e volta, e um coletor cobrando mark assist da goroutine que pontua o lance. É assim que o deadline é dividido, quais ajustes do Go compensam e o que um hot path em Rust não resolve.
Um bidder que perde leilões por timeout enquanto sua média parece saudável está sendo descrito pelo número errado. O deadline pertence à exchange e cobre a rede nos dois sentidos. Um coletor é uma propriedade do processo, então sua pausa é cobrada de todas as conexões de uma vez, e um pico em uma única conexão precisa de outra explicação.
Reescrever em Rust ou ajustar o runtime é uma escolha entre duas respostas para uma pergunta que ninguém fez: qual parte do deadline está sendo gasta, e em quê. O que vem a seguir separa o orçamento do coletor, o coletor do scheduler, e a reescrita do componente que a merece.
A resposta curta é estrutural. A exchange define o deadline e ele inclui a rede, então o primeiro conserto separa o p99 de uma conexão em tempo no handler, tempo esperando por um processador e tempo na rede. O que a amBrain pode sustentar publicamente: construímos o RTBBidder, uma demand-side platform entregue do zero, em que cada decisão de lance avalia dezenas de condições de targeting por impressão. Os números de latência que publicamos como medidos vêm de caminhos de trading, não de um bidder de anúncios, e nenhum número abaixo é medido em um bidder nosso.
A especificação OpenRTB define tmax como o tempo máximo em milissegundos que a exchange permite para receber lances, incluindo a latência da Internet, e diz que o valor prevalece sobre qualquer orientação anterior. O orçamento é uma ida e volta que chega dentro de cada requisição.
O limiar pelo qual você é julgado não é o do seu dashboard. A documentação do Authorized Buyers do Google exige que 85 por cento das respostas cheguem dentro do deadline medido no trading location, e restringe os bidders que não cumprem. Entre o relógio dele e o seu está tudo o que não é computação:
Então o deadline interno fica abaixo do tmax pela margem que o seu próprio histograma diz que uma resposta custa naquela conexão, rederivado por exchange em vez de definido uma única vez para a frota. Um deadline não é um plano de capacidade: o trabalho cancelado no deadline já gastou sua CPU.
Sob sobrecarga isso significa pagar o preço cheio por respostas que ninguém conta, então o conserto que falta é o controle de admissão: leia o tmax, compare-o com o atraso de fila que você mede, e responda no-bid quando a conta não fecha. Um no-bid rápido conta para os 85 por cento; um lance atrasado não conta.
Um garbage collector é uma propriedade do processo, então um ciclo disparado em qualquer conexão é cobrado de todas as conexões. A que falha primeiro é a que tem o tmax mais apertado e a requisição mais pesada. Descarte o que produz o mesmo quadro sem um coletor:
Nenhuma das quatro se conserta com um ajuste do coletor, e a separação tem instrumentos. Um profile de CPU separa as contas do coletor por símbolo, runtime.gcAssistAlloc para as cobranças no handler e runtime.gcBgMarkWorker para a marcação em background. O tempo de stop-the-world é /sched/pauses/total/gc:seconds, a espera em estado runnable é /sched/latencies:seconds, e a fila de accept é lida fora do processo, através de ListenOverflows.
Uma distinção decide de qual conserto no Go você precisa. Uma pausa stop-the-world é cobrada de todas as goroutines de uma vez, então aparece como um pico uniforme em todas as conexões. Um mark assist é cobrado da goroutine que alocou, então cai sobre as requisições que mais alocaram. Duas coisas reduzem um assist: menos bytes por bid request, ou um ciclo mais longo no qual os workers de background cobrem mais da marcação. Só a primeira sobrevive a uma mudança no mix de tráfego.
O coletor do Go é concorrente, e o guia oficial é explícito ao dizer que a duração da pausa não escala com o tamanho do heap, então as transições stop-the-world são breves. Os assists são a fonte que importa: as goroutines ajudam o coletor quando a alocação é rápida, porque a marcação em background recebe um quarto fixo dos processadores e o déficit é cobrado de quem aloca.
A taxa transforma isso em um limiar, não em uma rampa. A taxa de alocação é o QPS multiplicado pelos bytes por bid request contra uma parcela fixa de background, então um código que nunca faz assist a um quinto do seu tráfego pode fazer assist em quase toda requisição a 100K QPS.
O custo de marcação é proporcional ao grafo de ponteiros vivos, não ao lixo, e um bidder guarda o formato errado para isso: índices de campanhas, segmentos de audiência, caches de frequência. O Discord publicou a mesma constatação em 2020, com um coletor varrendo um cache LRU inteiro para decidir se a memória estava livre. Cinco mecanismos se escondem sob uma única expressão:
Leia isso como cinco contas separadas. Exatamente uma é quitada por um ajuste do coletor, e nenhuma por trocar de linguagem antes de a separação ser medida.
Gil Tene deu à falha o nome de coordinated omission: o sistema que mede se coordena com o sistema sob teste de um jeito que evita medir os outliers, porque uma malha fechada espera pela resposta e para de enviar durante uma paralisação. A ScyllaDB publicou em 2021 uma comparação em que uma carga reportou um p99 de 249 microssegundos em malha fechada e 665 ms sob carga aberta com correção, errando por cerca de 2,700 vezes.
Um gerador de carga que espera pela resposta para de enviar exatamente durante a paralisação que ele foi construído para encontrar, e depois dilui esse silêncio na média do resultado. O percentil que ele imprime em seguida descreve o gerador, não o seu bidder.
Nomeie o número de parada antes de ajustar, porque uma reescrita decidida por esgotamento não é uma decisão. Dois números, não um: alocações de heap por requisição, que governam o assist, e o heap vivo, que governa a marcação.
A ordem é fonte antes de teto, não maior ganho primeiro. Comece onde o assist é gerado: escape analysis no caminho do lance, buffers reaproveitados em vez de alocados, e um codec que lê os campos de que você precisa em vez de materializar um novo grafo de objetos. Mantenha as slices dele de vida curta: uma slice apontando para o buffer da requisição prende esse buffer inteiro por toda a vida do lance.
A abordagem tem um teto: a Uber relatou em 2021 que ajustar o GOGC contra o limite de memória do contêiner recuperou cerca de 70,000 cores nos seus serviços mission-critical. Isso é um resultado de custo, não um percentil.
A RTB House descreveu em junho de 2025 um serviço de bidding em JVM em que a divisão em microsserviços produziu um alto volume de requisições pequenas: a latência adicionada tinha que caber em 7 ms contra uma requisição média de cerca de 2.5 ms, e os percentis 98 e 99 quebraram sob pausas frequentes do G1. Eles migraram para o ZGC geracional e pagaram em memória.
Repare no que esse conserto exigiu: um segundo coletor para o qual migrar. O Go entrega um só e ele não é plugável, então as alavancas do Go são a taxa de alocação, o formato do conjunto vivo e o GOGC contra o GOMEMLIMIT. O que um hot path em Rust remove, em vez disso, é específico: sem assist, sem marcação em background, sem ciclo forçado. O que não vai embora é mais longo do que a maioria dos times espera:
Se a cauda mora no parsing da requisição, na conexão com a exchange ou no enfileiramento do scheduler, o Rust não devolve nenhum desses milissegundos, e reescrever o componente errado gasta um trimestre para manter a mesma taxa de timeout.
Então mova a menor peça que detém as alocações, não o serviço: o loop de avaliação de impressão e seus índices, a seleção de candidatos, targeting, consultas de frequência e de orçamento, scoring. Precifique a fronteira por travessia: uma vez por bid request com um buffer plano, nunca uma vez por regra de targeting. Valide em instâncias separadas alimentadas por um fluxo espelhado, nunca dentro do processo sob teste, onde um shadow path dobra as duas quantidades que você está medindo.
Um teste de aceitação que serve para os dois caminhos: rode uma janela curta com o coletor desligado, sob um teto de memória que você controla, e registre o p99.9 dentro do handler. Rode em uma instância atrás de uma fração do tráfego, com reversão automática, porque com o GOGC desligado um pico de heap contra o teto coloca o runtime em ciclos consecutivos, e o guia diz que essa paralisação pode ser indefinida. Leia o resultado apenas se o limitador de CPU do GC nunca tiver entrado em ação: assim que ele entra, o percentil descreve o limitador.
A segunda metade da pergunta, quem faz esse tipo de trabalho, tem um teste que não precisa de uma lista de fornecedores. Uma empresa que conserta latência causada pelo GC se comporta de forma diferente de uma que vende uma reescrita:
Nenhum deles exige confiança: cada um é um documento que você pode pedir na primeira conversa, e um que volta vago diz que o diagnóstico está sendo pulado.
Então a primeira pergunta não é qual linguagem. É qual das três somas - handler, scheduler ou rede - detém os milissegundos que faltam na conexão que dá timeout, e se os bytes alocados por bid request se movem quando você ataca isso.
O que a amBrain pode sustentar publicamente: a amBrain é uma empresa de desenvolvimento de software especializada em plataformas de trading, matching engines, sistemas de real-time bidding e engenharia de plataformas de cassino, construímos software desde 2019, e em AdTech o que construímos é desenvolvimento de DSP, plataformas de real-time bidding e engenharia de ad exchange. Trabalhamos em três formatos: entrega completa, time dedicado ou engenheiros embarcados no seu. Se você está pesando uma reescrita contra uma rodada de tuning, a conversa que vale ter é a que roda a separação antes de escolher uma linguagem.
Traga sua arquitetura atual e o modo de falha que preocupa você, e vamos analisá-lo juntos em meia hora.