Todos los exchanges publican límites de peticiones, y todos los hacen cumplir. Binance responde a una llamada REST limitada con un HTTP 429 y una cabecera Retry-After que se espera que respetes, y cierra cualquier conexión WebSocket que le envíe más de 5 mensajes por segundo. Dos informes de clientes llegaron con pocos días de diferencia, uno sobre cada mitad de este asunto, y entre los dos dejaron al descubierto tres problemas que merecía la pena corregir bien.
Los tres ya están corregidos. Uno de ellos resultó afectar a mucho más que a Binance.
Antes, una petición fallida tiraba sus cabeceras a la basura
Cuando una petición realizada a través de uno de los clientes de API ya preparados fallaba con un estado de error HTTP, el componente lanzaba una excepción con el código de estado, el texto de estado y el cuerpo. Las cabeceras de respuesta habían desaparecido. No estaban ocultas, sencillamente ya no estaban. El objeto HTTP se destruía mientras la excepción seguía subiendo por la pila, y las cabeceras se iban con él.
Eso hace imposible construir sobre el componente un backoff conforme a lo que exige Binance. Retry-After te dice exactamente cuánto hay que esperar, e ignorarlo hace que un 429 escale a un HTTP 418 y a un bloqueo temporal de la IP. Los contadores de límite que devuelve Binance, como X-MBX-USED-WEIGHT-1M, eran igual de inalcanzables, así que una aplicación tampoco podía regular su propio ritmo antes de darse contra el muro.
La excepción que se lanza ahora es EsgcHTTPAPIProtocolException, y lleva consigo las cabeceras de respuesta. Desciende de la excepción que se lanzaba antes, de modo que los manejadores existentes la siguen capturando y no hay que cambiar nada salvo que quieras la nueva información.
try
vResponse := oBinance.GetAggregateTrades('BTCUSDT');
except
on E: EsgcHTTPAPIProtocolException do
begin
if E.ErrorCode = 429 then
Sleep(E.RetryAfterMs);
vWeight := E.GetHeader('X-MBX-USED-WEIGHT-1M');
// E.ResponseHeaders holds the complete list
end;
end;
RetryAfterMs devuelve el retardo en milisegundos y entiende las dos formas que puede adoptar la cabecera, un simple número de segundos y una fecha HTTP. Devuelve -1 cuando la cabecera no está presente. La corrección se aplica a los ocho métodos de petición, no solo a Get, y el parámetro ResponseHeaders que Post y Query ya aceptaban ahora se rellena cuando una petición falla, que es justo cuando lo necesitas.
Conviene saber también que, desde 2026.7.0, el cliente HTTP puede encargarse de esperar por ti.
oBinance.RetryOptions.Enabled := True;
oBinance.RetryOptions.MaxRetries := 3;
// HonorRetryAfter is already True by default
Un frame en lugar de doce
El protocolo WebSocket de Binance acepta una lista de streams en un único frame SUBSCRIBE. El componente no lo estaba aprovechando. Cada método auxiliar Subscribe* escribía un frame con exactamente un stream, de inmediato, así que una lista de seguimiento corriente de seis símbolos en dos canales enviaba doce frames en unos pocos milisegundos. Binance cerraba la conexión, exactamente como está documentado.
Ahora hay métodos de agrupación que envían la lista completa como un solo frame:
oBinance.SubscribeStreams(['btcusdt@aggTrade', 'btcusdt@depth',
'ethusdt@aggTrade', 'ethusdt@depth']);
Un frame se queda muy por debajo del límite de 8 KB por petición incluso con cientos de streams, así que en la práctica una lista de seguimiento siempre es un frame. UnsubscribeStreams es la imagen especular.
La parte que nadie podía sortear
Este es el problema que más importaba, y ninguno de los dos informes lo detectó por completo.
Cada API de exchange guarda los canales a los que te has suscrito, para poder restaurarlos tras una reconexión. Esa reproducción enviaba un frame por stream, uno detrás de otro, sin ninguna regulación del ritmo, sobre una conexión que tenía unos pocos milisegundos de vida. Una lista de seguimiento de cualquier tamaño realista hacía que la conexión recién creada se cerrase de inmediato. El WatchDog volvía a conectar, repetía la misma ráfaga y la conexión se cerraba otra vez. Una única desconexión transitoria se convertía en un bucle de reconexión permanente.
Como esa ráfaga se genera dentro del componente, una aplicación no podía evitarla. Escribir tus propios frames en lugar de usar los métodos auxiliares tampoco servía, porque la reproducción no es algo que tú invoques.
Al auditar el resto de la librería se vio que el mismo bucle de reproducción se había copiado en 17 APIs de exchanges: Binance, Bitfinex, Bitget, Bitmex, Bitstamp, Bybit, Cex, CexPlus, Coinbase, CryptoCom, Deribit, GateIO, Huobi, Kraken, Kucoin, MEXC y OKX. Cada una contra el límite de su propio exchange.
Ahora hay una opción Throttle en los clientes de API WebSocket:
oBinance.Throttle.Enabled := True; // paces your own Subscribe calls
oBinance.Throttle.MaxMessages := 4; // default, leaves room for PING/PONG
oBinance.Throttle.IntervalMs := 1000; // default
El valor por defecto merece una explicación, porque es deliberadamente asimétrico. Throttle.Enabled es False, así que el ritmo de las llamadas que hace tu aplicación no cambia salvo que lo pidas. Pero Throttle.PaceResubscribe es True, así que la reproducción tras la reconexión se regula desde el primer momento.
El razonamiento es sencillo. Tú controlas tu propio bucle de suscripción y puedes agruparlo, así que regular el ritmo ahí es decisión tuya. La reproducción no la controlas, así que se regula por ti. El bucle de reconexión queda corregido para todas las aplicaciones existentes sin ningún cambio de código. En Binance la reproducción también se envía como frames combinados, de modo que una lista de seguimiento de doce streams ahora se reproduce en un único frame en lugar de doce. Pon PaceResubscribe a False si prefieres el comportamiento anterior.
Un detalle si usas varios exchanges: el presupuesto por defecto de 4 mensajes por segundo encaja con Binance, cuyo límite documentado es 5. OKX documenta 3, así que el cliente de OKX usa 3 por defecto. Comprueba el límite de tu exchange antes de subir MaxMessages.
Actualización
Actualización directa. La nueva excepción desciende de la que se lanzaba antes, los métodos de agrupación son añadidos, y Throttle está desactivado por defecto para todo salvo para la reproducción que rompía las reconexiones. Descarga la última versión desde la página de descarga de sgcWebSockets.
Los dos casos vinieron de clientes que se tomaron el tiempo de leer el código fuente, reproducir el problema y describirlo con precisión. Así es como se encontró el tercer error, el peor de todos. Si algo en los componentes se comporta de una forma que no puedes explicar, cuéntanoslo.
¿Preguntas, comentarios o ayuda con la migración? Ponte en contacto, recibirás respuesta de las personas que escribieron el código.
