Introducción
A lo largo de meses de iteración, el p99 de extremo a extremo de Lighter pasó de 280 ms a unos estables 55 ms. El p99 de procesamiento de transacciones pasó de picos de 20–30 ms a menos de 1 ms. El tiempo de aplicación del hot path se sitúa entre 100–250 µs. Todo en mainnet, a escala.
¿Cuánta escala? El 5 de junio de 2026, Lighter alcanzó un máximo histórico de 811 millones de transacciones en un solo día, con un promedio de 9,388 TPS y picos de 20,740 TPS.
El secuenciador de Lighter está escrito en Go. Las transacciones se ejecutan en Lighter L2 con pruebas criptográficas completas liquidadas en L1. La idea arquitectónica clave es que la demostración está desacoplada de la ejecución: la generación de pruebas se ejecuta de forma asíncrona y nunca bloquea el trading. Esto significa que la latencia que experimentan los traders está determinada únicamente por las capas de ejecución y API de Lighter.
La capa de API de Lighter es el servicio con el que los traders interactúan directamente. Mantiene una vista completa y actualizada del estado del exchange en memoria y sirve conexiones REST y WebSocket desde ese estado local. Una gorutina crítica recibe actualizaciones de estado del motor de ejecución y las aplica a las cachés en memoria en tiempo real. Todo lo que está aguas abajo (envíos WebSocket, lecturas de API, simulaciones de transacciones) se sirve desde estas cachés.
Conceptos Clave Cubiertos
Los siguientes conceptos proporcionan una visión general de alto nivel de los temas clave cubiertos en el resto de este informe técnico:
Encuéntralo antes de mainnet: Un entorno de prueba de carga idéntico con cuentas sintéticas y carga realista, instrumentado de manera mucho más agresiva que producción, detecta cuellos de botella antes de que los traders los noten.
Observabilidad de doble entorno: Las pruebas de carga llevan métricas demasiado costosas para producción; mainnet se monitorea en tiempo real para frescura, latencia de simulación y ciclo de vida de transacciones de extremo a extremo. Juntos, brindan una cobertura completa.
Muerte por mil asignaciones: Reemplazo sistemático de tipos pesados en heap y copias profundas con alternativas asignadas en la pila y snapshots inmutables. En un lenguaje con GC, menos asignaciones = menos pausas = menor latencia en la cola.
Control a nivel de SO: Fijar los hilos vitales a núcleos de CPU dedicados con prioridad casi en tiempo real.
Despliegue consciente de NUMA: La colocación consciente de NUMA garantiza que el hot path tenga acceso rápido a memoria local. La capa final en la pila de latencia.
Binario sobre conveniencia: Serialización binaria hecha a mano en el hot path elimina la reflexión y la sobrecarga de asignación. Cada microsegundo ahorrado se acumula en miles de actualizaciones por segundo.
Encontrar Cuellos de Botella Antes de que Lleguen a Mainnet
No puedes arreglar lo que no puedes ver. Antes de optimizar cualquier cosa, invertimos fuertemente en herramientas para identificar exactamente dónde se estaba gastando el tiempo.
El Entorno de Prueba de Carga
Cuando queremos probar algo, levantamos un entorno de prueba de carga dedicado: una copia idéntica de la infraestructura de mainnet ejecutando los servicios con configuraciones idénticas bajo la misma topología de despliegue. No siempre está funcionando; lo levantamos bajo demanda para una prueba específica y lo derribamos después.
Una vez que este entorno está activo, creamos cuentas sintéticas y generamos carga de trading realista para simular condiciones reales del mercado bajo estrés. Esto permite:
- Temporización extra-granular: Temporización paso a paso dentro del hot path, duración de construcción de caché por operación y marcas de tiempo del ciclo de vida de la transacción que rastrean cada etapa desde el envío hasta la confirmación.
- Perfilado bajo demanda y grabador de vuelo: Capturamos perfiles de CPU, memoria y trazas de ejecución a pedido durante la carga. El grabador de vuelo de Go nos da recolección de trazas siempre activa. Cuando se detecta un evento lento, los últimos segundos de ejecución se capturan automáticamente, permitiéndonos diagnosticar picos de latencia transitorios después del hecho.
- Trazado distribuido: Instrumentamos cada función importante en el hot path con spans de traza, dándonos visibilidad detallada de exactamente dónde se gasta el tiempo dentro de un solo ciclo de actualización, a través de los límites del servicio.
Monitoreo de Doble Entorno
Monitoreamos ambos entornos de cerca (mainnet y prueba de carga) pero a diferentes niveles de granularidad.
Mainnet se monitorea estrechamente en tiempo real. Rastreamos todo lo que importa para la experiencia de trading:
- Seguimiento de frescura: Medimos la latencia a través del canal WebSocket
order_booken los mercados más activos. Esencialmente, la diferencia entre cuándo se produce una actualización del libro de órdenes y cuándo un cliente la recibe. Esta es la métrica que refleja más directamente la experiencia que sienten los traders: ¿qué tan desactualizado está el libro de órdenes que estás viendo? Rastreamos esto en dos puntos. El más importante es motor de ejecución → cliente: el pipeline completo desde que el motor de ejecución procesa un cambio de estado hasta el momento en que la actualizaciónorder_bookresultante llega al cliente. También medimos capa de API → cliente: la capa de API marca la hora de cada actualización saliente y el cliente la compara con su propio reloj, dándonos la latencia de última milla de forma aislada. - Latencia de simulación en seco: La capa de API simula cada transacción en seco (validando firmas, nonces y saldos) antes de pasarla al motor de ejecución. Cada simulación en seco se cronometra y se reporta.
- Ciclo de vida de transacción de extremo a extremo: Histogramas de latencia de ciclo completo desde el envío hasta la confirmación.
- Efectividad de caché: Con qué frecuencia servimos desde memoria vs. recurrir a almacenamiento más lento.
Prueba de carga se monitorea aún más granularmente. Como no está sirviendo a traders reales, podemos aumentar la instrumentación sin preocuparnos por la sobrecarga: temporización paso a paso dentro del hot path, perfilado de asignaciones, cubos de histograma adicionales en cada mutación de caché. Esta granularidad más fina detecta regresiones a nivel micro que las métricas seguras para producción de mainnet no revelarían.
El monitoreo de mainnet nos dice cómo está funcionando el sistema para los traders reales ahora mismo. El monitoreo de prueba de carga detecta regresiones antes de que lleguen a producción.
Eliminación de Copias Profundas y Guerra de Asignaciones en Heap
El recolector de basura de Go es un impuesto a la latencia. Cada asignación en heap eventualmente se convierte en una pausa de GC, y las pausas de GC en un motor de trading son picos de latencia para los traders. Así que revisamos el hot path y redujimos las asignaciones donde pudimos. El resultado fue menos picos de latencia y un rendimiento más predecible.
Eliminación de Copias Profundas
Las copias profundas son fábricas de asignaciones. Las abordamos de varias maneras:
- Snapshots inmutables: Hicimos que las cachés del libro de órdenes fueran inmutables. Las lecturas devuelven un puntero al snapshot actual, sin necesidad de copia. Las actualizaciones crean una nueva versión y la intercambian atómicamente mediante
atomic.Pointer. - Eliminación de copias innecesarias: Encontramos rutas de código que copiaban profundamente datos que nunca se mutaban posteriormente. Las eliminamos por completo.
Diseño Consciente de Asignaciones
- Numéricos asignados en la pila: Reemplazamos
big.Intybig.Ratpesados en heap con alternativas asignadas en la pila (int128,float64, divisiónint64) en todo el hot path: conversiones de precio, mapas de profundidad del libro de órdenes, cálculos de tamaño. Hasta 8.3× más rápido en funciones clave de formato, cero asignaciones en heap por operación. - Actualizaciones condicionales: Omitir asignación cuando nada cambió.
- Colecciones pre-dimensionadas: Eliminar ciclos de crecimiento y copia dimensionando las estructuras de datos de antemano.
- Reutilización de buffers: La ruta del suscriptor deserializa miles de actualizaciones por segundo. Redujimos las asignaciones con reutilización basada en pool de buffers intermedios.
Esto aplanó la cola. Antes, el p99 de extremo a extremo de Lighter (motor de ejecución a cliente, medido en el canal WebSocket order_book) se disparaba a 200–280 ms durante períodos con muchas asignaciones. Después de la eliminación de copias profundas, las cachés inmutables y el trabajo de asignaciones en heap, el p99 se estabilizó en una banda plana de ~50–60 ms sin prácticamente picos:

Latencia de Extremo a Extremo: Motor de Ejecución -> Cliente (p99)
La presión del GC en la capa de API también disminuyó notablemente, pero esto no fue solo un cambio. Rediseñamos fundamentalmente cómo operan las cachés del servidor de API. Anteriormente, las cachés tenían caducidad basada en TTL y asignaban nuevos objetos en cada actualización. Cada una de esas asignaciones de corta duración se convertía en trabajo de GC. Después del rediseño, el estado completo del exchange se inicia en caliente desde un snapshot y se mantiene en memoria como estructuras inmutables de larga duración que se intercambian atómicamente. Eso eliminó el desgaste del TTL y las asignaciones por actualización, y la presión del GC disminuyó con ellas. Monitoreamos de cerca el uso de memoria y el conjunto de trabajo está acotado.
Ejecutamos el servidor de API antiguo y el rediseñado lado a lado con tráfico de mainnet. La duración de la pausa de GC (p75) del servidor rediseñado se situó alrededor de ~3 ms, en comparación con ~5–6 ms en el servidor antiguo. Aproximadamente la mitad del tiempo de pausa de GC:

Duración de Pausa de GC: Servidor de API Estándar vs. Snapshot
La frecuencia cuenta una historia aún más clara. El servidor de API antiguo activaba ciclos de GC ~2.2× más a menudo que el rediseñado, reflejando directamente menos asignaciones de corta duración y menos presión general de GC:

Frecuencia de Ciclos de GC: Servidor de API Estándar vs. Servidor de API Snapshot
Ajuste de GOGC en el Motor de Ejecución
Aplicamos un pensamiento similar sobre GC al propio motor de ejecución. El parámetro GOGC de Go controla qué tan agresivamente se ejecuta el recolector de basura. El valor predeterminado intercambia tiempo de CPU por eficiencia de memoria, pero para una ruta crítica de latencia, el intercambio era incorrecto.
Después de ajustar GOGC, la duración de GC del motor de ejecución cayó de un promedio de ~30 µs con picos que alcanzaban 100 µs, a una banda estable de ~10 µs. Una reducción de ~3× sin prácticamente picos:

Duración de GC del Motor de Ejecución Después del Ajuste de GOGC
Tres meses de datos de mainnet confirman que la mejora se mantuvo: el período posterior es plano y predecible.
El procesamiento de transacciones mejoró con ello. Durante 90 días de datos de mainnet que cubren todos los tipos de transacciones (crear orden, cancelar, liquidar, desapalancar, transferir y más), el p99 pasó de picos frecuentes de 20–30 ms antes del ajuste de GOGC a principalmente por debajo de 1 ms después, con valores atípicos ocasionales de 3–4 ms. La mejora se ha mantenido durante más de dos meses:

Tiempos de Procesamiento de Transacciones - p99 (Últimos 90 días)
Servicio de Snapshot — Menos Datos, Despliegues en Caliente
Después de eliminar la sobrecarga de asignaciones, abordamos el siguiente cuello de botella: el volumen de datos que fluye a través del bus de mensajes interno. Construimos un servicio de snapshot que mantiene snapshots completos del estado en memoria. Esto tuvo dos efectos principales:
- Menos datos en el cable. Con el servicio de snapshot manteniendo el estado completo, el motor de ejecución ya no necesita enviar el estado completo a través del bus de mensajes en cada actualización. Escribe menos datos, lo que significa menos ancho de banda de red consumido y menos trabajo de deserialización en el extremo receptor.
- Despliegues sin calentamiento. Al desplegar, la capa de API arranca desde un snapshot (cuentas, libros de órdenes, información de mercado, claves públicas de API) y está inmediatamente lista para servir. No hay período de calentamiento mientras se llenan las cachés. Después de cargar el snapshot, la capa de API se suscribe al flujo de actualizaciones y aplica deltas en tiempo real.
Además de la base del snapshot, las cachés en memoria se actualizan continuamente:
- Información de cuenta: Lecturas sin bloqueo mediante
sync.Map, intercambios de puntero atómico para actualizaciones. - Libros de órdenes: Almacenados como snapshots inmutables. Las lecturas obtienen un puntero, las actualizaciones intercambian una nueva versión. Sin bloqueos en la ruta de lectura.
- Caché de claves de API: Todas las claves caben en memoria. Se eliminaron por completo las búsquedas externas.
El resultado: todo el estado del exchange reside en memoria local, actualizado en tiempo real, y cada despliegue comienza en caliente.
El efecto dominó en el motor de ejecución fue significativo. Anteriormente, el motor de ejecución escribía claves de caché a través de la red, actualizaciones que los servicios leían de vez en cuando. Con todo viviendo en memoria y el servicio de snapshot manejando la distribución del estado, esas escrituras de red se volvieron innecesarias. Las eliminamos. El resultado: los tiempos de bloque p99 cayeron de ~2.6 ms a ~1.2–1.8 ms, simplemente porque el motor de ejecución escribe mucho menos ahora:

Tiempos de Bloque - p99
Planificación de CPU
Una vez que optimizamos todo en el espacio de usuario, llegamos al kernel.
Tenemos muchas gorutinas y necesitan ser programadas en la CPU para ejecutarse. Minimizar la sobrecarga de planificación es importante para sistemas de baja latencia. Por defecto, el runtime de Go multiplexa las gorutinas a través de hilos del SO, y el SO puede migrar hilos libremente entre núcleos de CPU. Ambos introducen latencia impredecible.
Eliminamos esto apilando cuatro mecanismos:
- `runtime.LockOSThread()`: Bloquea la gorutina a un solo hilo del SO, evitando que el planificador de Go la migre.
- Afinidad de CPU mediante `sched_setaffinity`: Fija ese hilo del SO a un núcleo de CPU específico (Linux). Esto evita que el kernel lo migre entre núcleos, evitando la invalidación de caché L1/L2.
- Planificación de alta prioridad mediante SCHED_FIFO: Eleva la prioridad de planificación del hilo, asegurando que el kernel lo favorezca sobre otro trabajo.
- Bucle de espera activa (spin loop): El hot path ejecuta un
selectcon un casodefaultvacío, por lo que la gorutina nunca se estaciona. Sin él, Go mueve la gorutina a un estado "ejecutable" cuando no hay datos disponibles, y la reprogramación añade latencia de activación. Con el bucle de espera activa, la gorutina permanece ejecutándose en su núcleo fijado y recoge nuevas actualizaciones con cero retraso de planificación.
Medir el impacto correctamente es importante aquí. El tiempo de aplicación varía con las condiciones del tráfico, por lo que los números absolutos cambian con la carga. Para aislar el efecto de la fijación, ejecutamos dos grupos de servidores de API lado a lado bajo el mismo tráfico. Un grupo permaneció sin fijar como control, y cambiamos el otro a fijación de CPU. La diferencia porcentual entre ellos, medida al mismo tiempo bajo carga idéntica, cuenta la historia real.
Antes de la fijación, ambos grupos se rastrean juntos. Misma latencia base, mismo comportamiento de picos:

Tiempo de Aplicación del Hot Path - Antes de la Planificación de CPU
Después de habilitar la fijación en un grupo, este se sitúa consistentemente por debajo de la línea base sin fijar. Mismo tráfico, menor latencia. Los picos del grupo fijado también están limitados más abajo, porque la fluctuación por migración de hilos y la invalidación de caché L1/L2 se eliminan:

Tiempo de Aplicación del Hot Path - Después de la Planificación de CPU
Despliegue Consciente de NUMA
La fijación de CPU por sí sola no es suficiente si los accesos a memoria del núcleo fijado cruzan los límites de NUMA. Un nodo NUMA es un grupo de CPUs con su propia memoria local. Acceder a memoria desde un nodo NUMA remoto conlleva una penalización de 10× en comparación con el acceso local.
Los servidores de API de Lighter se ejecutaban originalmente en máquinas más grandes con 2 nodos NUMA:
1$ lscpu | grep NUMA2NUMA node(s): 23NUMA node0 CPU(s): 0-954NUMA node1 CPU(s): 96-19156$ cat /sys/devices/system/node/node0/distance710 100
La matriz de distancias cuenta la historia: el acceso local cuesta 10, el acceso entre nodos cuesta 100. Una penalización de 10×. Con N servidores de API compartiendo la máquina, algunos inevitablemente tenían su CPU fijada en un nodo NUMA y su memoria de trabajo (cachés en memoria, buffers de actualización) en el otro. Cada iteración del hot path estaba pagando el impuesto entre nodos.
La solución fue contraintuitiva: movimos los servidores de API a máquinas más pequeñas con un solo nodo NUMA. La mitad de las especificaciones, pero todos los accesos a memoria ahora están garantizados como locales:
1$ lscpu | grep NUMA2NUMA node(s): 13NUMA node0 CPU(s): 0-95
Esto redujo costos y mejoró la latencia al mismo tiempo. El tiempo de aplicación del hot path cayó aún más al rango de ~100–250 µs, con períodos de baja actividad bajando a ~100 µs. Compara esto con el rango de ~200–520 µs con solo fijación de CPU en las máquinas de 2 nodos NUMA:

Tiempo de Aplicación del Hot Path - Optimización NUMA
Serialización Binaria Personalizada
El motor de ejecución de Lighter publica actualizaciones de estado a la capa de API a través de un bus de mensajes interno. Cada actualización del sistema fluye a través de esta ruta. La serialización original usaba una biblioteca de codificación de propósito general, pero la reflexión, los cambios de tipo y las asignaciones por campo creaban una sobrecarga innecesaria en un hot path.
Reemplazamos eso con serialización binaria hecha a mano: diseño de diseño fijo, codificación/decodificación sin reflexión para cada tipo de entidad en el sistema. Cada tipo tiene un codificador/decodificador dedicado que lee y escribe campos en desplazamientos de bytes conocidos. Sin reflexión ni cambios de tipo, y asignaciones mínimas. Cada códec tiene pruebas de ida y vuelta y pruebas difusas para detectar regresiones.
El resultado fue una sobrecarga de serialización significativamente menor en la ruta que alimenta cada caché aguas abajo.
Hay evaluaciones comparativas independientes disponibles a través de un panel construido y mantenido por el miembro de la comunidad @UngusTrade, que compara latencias de transacciones en vivo entre venues de trading perpetuo: latency.perps.trading





![[Edición Definitiva] Un análisis profundo de Recruit Holdings por un ex empleado (escrito por mí, no por IA)](/cdn-cgi/image/width=1920,quality=90,format=auto,metadata=none/https%3A%2F%2Fcms-assets.youmind.com%2Fmedia%2F1783619810056_4fyuz8_HMtxHM_bIAA-0_J.jpg)