Un bidder en Go que pierde subastas por timeout mientras su media parece sana tiene dos problemas bajo un mismo síntoma: un deadline que pertenece al exchange y cubre el viaje de ida y vuelta, y un recolector que le cobra el mark assist a la goroutine que puntúa la puja. Así se divide el deadline, qué parámetros de Go compensan y qué no arregla un hot path en Rust.
A un bidder que pierde subastas por timeout mientras su media parece sana lo describe el número equivocado. El deadline pertenece al exchange y cubre la red en ambos sentidos. Un recolector es una propiedad del proceso, así que su pausa se le cobra a todas las conexiones a la vez, y un pico en una sola conexión necesita otra explicación.
Reescribir en Rust o ajustar el runtime es elegir entre dos respuestas a una pregunta que nadie ha hecho: qué parte del deadline se está gastando, y en qué. Lo que sigue separa el presupuesto del recolector, el recolector del scheduler, y la reescritura del componente que la merece.
La respuesta corta es estructural. El exchange fija el deadline, y este incluye la red, así que la primera reparación divide el p99 de una conexión en tiempo de handler, tiempo esperando a un procesador y tiempo en la red. Lo que amBrain puede sustentar públicamente: construimos RTBBidder, una demand-side platform entregada desde cero, donde cada decisión de puja evalúa decenas de condiciones de segmentación por impresión. Las cifras de latencia que publicamos como medidas vienen de rutas de trading, no de un bidder publicitario, y ningún número de abajo está medido en un bidder nuestro.
La especificación de OpenRTB define tmax como el tiempo máximo en milisegundos que el exchange permite para recibir pujas, incluida la latencia de Internet, y dice que ese valor prevalece sobre cualquier indicación previa. El presupuesto es un viaje de ida y vuelta que llega dentro de cada solicitud.
El umbral con el que te juzgan no es el de tu dashboard. La documentación de Authorized Buyers de Google exige que el 85 por ciento de las respuestas lleguen dentro del deadline medido en el trading location, y limita a los bidders que no lo cumplen. Entre su reloj y el tuyo está todo lo que no es cómputo:
Así que el deadline interno queda por debajo de tmax en la cantidad que tu propio histograma dice que cuesta una respuesta en esa conexión, recalculado por exchange en vez de fijado una sola vez para toda la flota. Un deadline no es un plan de capacidad: el trabajo cancelado en el deadline ya ha gastado su CPU.
Bajo sobrecarga eso significa pagar el precio completo por respuestas que nadie cuenta, así que la reparación que falta es el control de admisión: lee tmax, compáralo con el retraso de cola que mides y responde no-bid cuando la aritmética no cuadre. Un no-bid rápido cuenta para el 85 por ciento; una puja tardía no.
Un recolector de basura es una propiedad del proceso, así que un ciclo disparado en cualquier conexión se le cobra a todas. La primera que incumple es la que tiene el tmax más ajustado y la solicitud más pesada. Descarta lo que produce la misma imagen sin recolector:
Ninguno de los cuatro se repara con un ajuste del recolector, y el desglose tiene instrumentos. Un perfil de CPU separa por símbolo las facturas del recolector: runtime.gcAssistAlloc para los cargos del handler y runtime.gcBgMarkWorker para el marcado en segundo plano. El tiempo stop-the-world es /sched/pauses/total/gc:seconds, la espera en runnable es /sched/latencies:seconds, y la cola de accept se lee fuera del proceso a través de ListenOverflows.
Una distinción decide qué reparación de Go necesitas. Una pausa stop-the-world se le cobra a todas las goroutines a la vez, así que aparece como un pico plano en todas las conexiones. Un mark assist se le cobra a la goroutine que asignó, así que cae sobre las solicitudes que más asignaron. Dos cosas bajan un assist: menos bytes por solicitud de puja, o un ciclo más largo en el que los workers de fondo cubren más marcado. Solo la primera sobrevive a un cambio en la mezcla de tráfico.
El recolector de Go es concurrente, y la guía oficial es explícita en que la duración de la pausa no escala con el tamaño del heap, así que las transiciones stop-the-world son breves. Los assists son la fuente que importa: las goroutines asisten al recolector cuando la asignación es rápida, porque el marcado en segundo plano recibe un cuarto fijo de los procesadores y el déficit se le cobra a quien asigna.
La tasa convierte esto en un umbral y no en una pendiente. La tasa de asignación es QPS multiplicado por bytes por solicitud de puja frente a una parte de fondo fija, así que un código que nunca hace assist con una quinta parte de tu tráfico puede hacer assist en casi todas las solicitudes a 100K QPS.
El coste del marcado es proporcional al grafo de punteros vivos, no a la basura, y un bidder tiene la forma equivocada para eso: índices de campañas, segmentos de audiencia, cachés de frecuencia. Discord publicó el mismo hallazgo en 2020, con un recolector escaneando una caché LRU entera para decidir si la memoria estaba libre. Cinco mecanismos se esconden bajo una sola frase:
Léelo como cinco facturas separadas. Exactamente una se salda con un ajuste del recolector, y ninguna cambiando el lenguaje antes de medir el desglose.
Gil Tene bautizó el fallo como coordinated omission: el sistema de medición se coordina con el sistema bajo prueba de una forma que evita medir los valores atípicos, porque un lazo cerrado espera una respuesta y deja de enviar durante un atasco. ScyllaDB publicó en 2021 una comparación donde una carga reportó un p99 de 249 microsegundos en lazo cerrado y de 665 ms bajo carga abierta con corrección, desviado por un factor de unas 2,700 veces.
Un generador de carga que espera una respuesta deja de enviar justo durante el atasco que se construyó para encontrar, y luego promedia el silencio dentro del resultado. El percentil que imprime después describe al generador, no a tu bidder.
Nombra la cifra de parada antes de ajustar, porque una reescritura decidida por agotamiento no es una decisión. Dos cifras, no una: las asignaciones de heap por solicitud, que impulsan el assist, y el heap vivo, que impulsa el marcado.
El orden es fuente antes que techo, no la mayor ganancia primero. Empieza donde se genera el assist: escape analysis en la ruta de puja, buffers reutilizados en vez de asignados, y un codec que lea los campos que necesitas en vez de materializar un grafo de objetos nuevo. Mantén sus slices de vida corta: un slice sobre el buffer de la solicitud fija ese buffer entero durante toda la vida de la puja.
El enfoque tiene un techo: Uber reportó en 2021 que ajustar GOGC contra el límite de memoria del contenedor recuperó unos 70,000 núcleos en sus servicios críticos. Eso es un resultado de coste, no un percentil.
RTB House describió en junio de 2025 un servicio de pujas en JVM donde una división en microservicios produjo un gran volumen de solicitudes pequeñas: la latencia añadida tenía que quedarse dentro de 7 ms frente a una solicitud media de unos 2.5 ms, y los percentiles 98 y 99 se rompían con pausas frecuentes de G1. Migraron a ZGC generacional y lo pagaron en memoria.
Fíjate en lo que exigió esa reparación: un segundo recolector al que cambiar. Go trae uno y no es intercambiable, así que las palancas de Go son la tasa de asignación, la forma del conjunto vivo y GOGC frente a GOMEMLIMIT. Lo que en cambio elimina un hot path en Rust es concreto: sin assist, sin marcado en segundo plano, sin ciclo forzado. Lo que no desaparece es más largo de lo que la mayoría de los equipos espera:
Si la cola vive en el parseo de la solicitud, en la conexión con el exchange o en el encolado del scheduler, Rust no devuelve ninguno de esos milisegundos, y reescribir el componente equivocado gasta un trimestre para conservar la misma tasa de timeouts.
Así que mueve la pieza más pequeña que se lleva las asignaciones, no el servicio: el bucle de evaluación de impresiones y sus índices, la selección de candidatos, la segmentación, las consultas de frecuencia y presupuesto, el scoring. Pon precio a la frontera por cada cruce: una vez por solicitud de puja con un buffer plano, nunca una vez por regla de segmentación. Valida en instancias separadas alimentadas con un flujo espejo, nunca dentro del proceso bajo prueba, donde una ruta en sombra duplica las dos magnitudes que estás midiendo.
Una prueba de aceptación que sirve para cualquiera de los dos caminos: ejecuta una ventana corta con el recolector desactivado, bajo un techo de memoria que controlas, y registra el p99.9 dentro del handler. Hazlo en una sola instancia detrás de una fracción del tráfico y con reversión automática, porque con GOGC desactivado un pico de heap contra el techo mete al runtime en ciclos consecutivos, y la guía dice que ese atasco puede ser indefinido. Lee el resultado solo si el limitador de CPU del GC nunca se activó: en cuanto lo hace, el percentil describe al limitador.
La segunda mitad de la pregunta, quién hace este tipo de trabajo, tiene una prueba que no necesita una lista de proveedores. Una empresa que repara latencia causada por el GC se comporta de forma distinta a una que vende una reescritura:
Ninguno exige confianza: cada uno es un documento que puedes pedir en la primera conversación, y uno que vuelve vago dice que se está saltando el diagnóstico.
Así que la primera pregunta no es qué lenguaje. Es cuál de las tres sumas - handler, scheduler o red - se lleva los milisegundos que faltan en la conexión que agota el tiempo, y si los bytes asignados por solicitud de puja se mueven cuando lo atacas.
Lo que amBrain puede sustentar públicamente: amBrain es una empresa de desarrollo de software especializada en plataformas de trading, matching engines, sistemas de real-time bidding e ingeniería de plataformas de casino, llevamos construyendo software desde 2019, y en AdTech lo que construimos es desarrollo de DSP, plataformas de real-time bidding e ingeniería de ad exchange. Trabajamos en tres formatos: entrega completa, un equipo dedicado o ingenieros integrados en el tuyo. Si estás sopesando una reescritura frente a una tanda de tuning, la conversación que merece la pena hace el desglose antes de elegir un lenguaje.
Traiga su arquitectura actual y el modo de fallo que le preocupa, y lo repasaremos juntos en media hora.