Rate limit di Binance in Delphi: batching, pacing e 429 leggibili | eSeGeCe Blog

Rate limit di Binance in Delphi: batching, pacing e 429 leggibili

· Componenti

Ogni exchange pubblica i propri rate limit e ogni exchange li fa rispettare. Binance risponde a una chiamata REST che ha superato il limite con un HTTP 429 e un header Retry-After che ci si aspetta venga rispettato, e chiude una connessione WebSocket che gli invia più di 5 messaggi al secondo. Due segnalazioni di clienti sono arrivate a pochi giorni di distanza l'una dall'altra, una per ciascuna metà della questione, e insieme hanno messo in luce tre problemi che valeva la pena risolvere per bene.

Tutti e tre ora sono risolti. Uno di essi si è rivelato riguardare molto più di Binance.

Una richiesta fallita buttava via i suoi header

Quando una richiesta effettuata tramite uno dei client API già pronti falliva con uno stato di errore HTTP, il componente sollevava un'eccezione che portava con sé il codice di stato, il testo dello stato e il body. Gli header di risposta erano spariti. Non nascosti, spariti. L'oggetto HTTP veniva distrutto mentre l'eccezione stava ancora risalendo lo stack, e gli header se ne andavano con lui.

Questo rende impossibile costruire sopra il componente un backoff conforme alle regole di Binance. Retry-After indica esattamente quanto tempo attendere e ignorarlo fa passare da un 429 a un HTTP 418 e a un ban temporaneo dell'IP. I contatori di rate limit restituiti da Binance, come X-MBX-USED-WEIGHT-1M, erano altrettanto irraggiungibili, quindi un'applicazione non poteva nemmeno regolare il proprio ritmo prima di sbattere contro il muro.

L'eccezione sollevata ora è EsgcHTTPAPIProtocolException e porta con sé gli header di risposta. Discende dall'eccezione sollevata in precedenza, quindi i gestori esistenti continuano a intercettarla e non serve modificare nulla, a meno che non si vogliano le nuove informazioni.

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 restituisce il ritardo in millisecondi e comprende entrambe le forme che l'header può assumere, un semplice numero di secondi e una data HTTP. Restituisce -1 quando l'header non è presente. La correzione si applica a tutti e otto i metodi di richiesta, non solo a Get, e il parametro ResponseHeaders che Post e Query già accettavano ora viene popolato quando una richiesta fallisce, ossia esattamente quando serve.

Vale la pena saperlo insieme a tutto questo: dalla versione 2026.7.0 il client HTTP può anche occuparsi dell'attesa al posto tuo.

oBinance.RetryOptions.Enabled := True;
oBinance.RetryOptions.MaxRetries := 3;
// HonorRetryAfter is already True by default

Un frame invece di dodici

Il protocollo WebSocket di Binance accetta un elenco di stream in un unico frame SUBSCRIBE. Il componente non lo utilizzava. Ogni helper Subscribe* scriveva immediatamente un frame che trasportava esattamente uno stream, quindi una normale watchlist di sei simboli su due canali inviava dodici frame nel giro di pochi millisecondi. Binance chiudeva la connessione, esattamente come documentato.

Ora esistono metodi di batching che inviano l'intero elenco come un unico frame:

oBinance.SubscribeStreams(['btcusdt@aggTrade', 'btcusdt@depth',
  'ethusdt@aggTrade', 'ethusdt@depth']);

Un frame resta ben al di sotto del limite di 8 KB per richiesta anche con centinaia di stream, quindi in pratica una watchlist è sempre un solo frame. UnsubscribeStreams è l'immagine speculare.

La parte che nessuno poteva aggirare

Questo è il problema che contava di più, e nessuna delle due segnalazioni lo aveva colto del tutto.

Ogni API di exchange conserva i canali sottoscritti, in modo da poterli ripristinare dopo una riconnessione. Quel replay inviava un frame per ogni stream, uno dopo l'altro, senza alcun controllo del ritmo, su una connessione vecchia di pochi millisecondi. Una watchlist di dimensioni anche solo realistiche faceva chiudere subito la connessione appena aperta. Il WatchDog si riconnetteva, ripeteva la stessa raffica e veniva chiuso di nuovo. Una singola disconnessione temporanea si trasformava in un ciclo di riconnessione permanente.

Poiché quella raffica viene generata all'interno del componente, un'applicazione non poteva evitarla. Scrivere i propri frame invece di usare gli helper non serviva a nulla, dato che il replay non è qualcosa che si invoca.

L'analisi del resto della libreria ha mostrato che lo stesso ciclo di replay era stato copiato in 17 API di exchange: Binance, Bitfinex, Bitget, Bitmex, Bitstamp, Bybit, Cex, CexPlus, Coinbase, CryptoCom, Deribit, GateIO, Huobi, Kraken, Kucoin, MEXC e OKX. Ognuna contro il limite del proprio exchange.

Ora è disponibile un'opzione Throttle sui client 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

Il valore predefinito merita una spiegazione, perché è volutamente asimmetrico. Throttle.Enabled è False, quindi la temporizzazione delle chiamate effettuate dalla tua applicazione non cambia se non lo richiedi. Ma Throttle.PaceResubscribe è True, quindi il replay alla riconnessione viene ritmato fin da subito.

Il ragionamento è semplice. Il ciclo di sottoscrizione lo controlli tu e puoi raggrupparlo, quindi il ritmo lì è una tua decisione. Il replay non lo controlli, quindi viene ritmato per te. Il ciclo di riconnessione è risolto per ogni applicazione esistente senza alcuna modifica al codice. Su Binance il replay viene inoltre inviato come frame combinati, quindi una watchlist di dodici stream ora viene ripetuta come un unico frame anziché dodici. Imposta PaceResubscribe a False se vuoi il comportamento precedente.

Un dettaglio se utilizzi diversi exchange: il budget predefinito di 4 messaggi al secondo è adatto a Binance, il cui limite documentato è 5. OKX documenta 3, quindi il client OKX usa 3 come valore predefinito. Verifica il limite del tuo exchange prima di aumentare MaxMessages.

Aggiornamento

Sostituzione diretta. La nuova eccezione discende da quella sollevata in precedenza, i metodi di batching sono aggiunte e Throttle è disattivato per impostazione predefinita per tutto tranne che per il replay che rompeva le riconnessioni. Scarica l'ultima versione dalla pagina di download di sgcWebSockets.

Entrambe queste segnalazioni sono arrivate da clienti che si sono presi il tempo di leggere il sorgente, riprodurre il problema e descriverlo con precisione. È così che è stato trovato il terzo e peggiore bug. Se qualcosa nei componenti si comporta in un modo che non riesci a spiegarti, faccelo sapere.

Domande, feedback o aiuto per la migrazione? Contattaci, riceverai una risposta dalle persone che hanno scritto il codice.