Binance Rate-Limits in Delphi: Batching, Taktung und lesbare 429er | eSeGeCe Blog

Binance Rate-Limits in Delphi: Batching, Taktung und lesbare 429er

· Komponenten

Jede Börse veröffentlicht Rate-Limits, und jede Börse setzt sie durch. Binance beantwortet einen ratenbegrenzten REST-Aufruf mit einem HTTP 429 und einem Retry-After-Header, den Sie beachten sollen, und schließt eine WebSocket-Verbindung, die mehr als 5 Nachrichten pro Sekunde sendet. Zwei Kundenmeldungen trafen innerhalb weniger Tage ein, je eine zu einer der beiden Hälften, und zusammen brachten sie drei Probleme ans Licht, die eine ordentliche Lösung verdient hatten.

Alle drei sind jetzt behoben. Eines davon betraf am Ende weit mehr als nur Binance.

Eine fehlgeschlagene Anfrage hat ihre Header früher weggeworfen

Wenn eine Anfrage über einen der fertigen API-Clients mit einem HTTP-Fehlerstatus fehlschlug, löste die Komponente eine Exception aus, die den Statuscode, den Statustext und den Body enthielt. Die Response-Header waren weg. Nicht versteckt, weg. Das HTTP-Objekt wurde freigegeben, während die Exception noch den Stack hinauflief, und die Header verschwanden mit ihm.

Damit lässt sich auf Basis der Komponente kein regelkonformes Binance-Backoff bauen. Retry-After sagt Ihnen genau, wie lange Sie warten müssen, und wer den Header ignoriert, wird vom 429 zu einem HTTP 418 und einer zeitweiligen IP-Sperre hochgestuft. Die Rate-Limit-Zähler, die Binance zurückgibt, etwa X-MBX-USED-WEIGHT-1M, waren genauso unerreichbar, sodass sich eine Anwendung auch nicht selbst bremsen konnte, bevor sie gegen die Wand lief.

Die ausgelöste Exception ist jetzt EsgcHTTPAPIProtocolException, und sie trägt die Response-Header mit sich. Sie leitet sich von der zuvor ausgelösten Exception ab, sodass bestehende Handler sie weiterhin abfangen und sich nichts ändern muss, sofern Sie die neuen Informationen nicht nutzen wollen.

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 liefert die Wartezeit in Millisekunden und versteht beide Formen, die der Header annehmen darf, eine einfache Anzahl von Sekunden und ein HTTP-Datum. Der Rückgabewert ist -1, wenn der Header fehlt. Die Korrektur gilt für alle acht Anfragemethoden, nicht nur für Get, und der Parameter ResponseHeaders, den Post und Query bereits akzeptierten, wird jetzt auch dann gefüllt, wenn eine Anfrage fehlschlägt, also genau dann, wenn Sie ihn brauchen.

Ergänzend gut zu wissen: Seit 2026.7.0 kann der HTTP-Client das Warten auch für Sie übernehmen.

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

Ein Frame statt zwölf

Das Binance-WebSocket-Protokoll akzeptiert eine Liste von Streams in einem einzigen SUBSCRIBE-Frame. Die Komponente hat das nicht genutzt. Jeder Subscribe*-Helfer schrieb sofort einen Frame mit genau einem Stream, sodass eine gewöhnliche Watchlist aus sechs Symbolen über zwei Kanäle zwölf Frames innerhalb weniger Millisekunden sendete. Binance schloss die Verbindung, genau wie dokumentiert.

Es gibt jetzt Batching-Methoden, die die gesamte Liste als einen Frame senden:

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

Ein Frame bleibt selbst mit Hunderten von Streams weit unter dem Anfragelimit von 8 KB, in der Praxis ist eine Watchlist also immer ein einziger Frame. UnsubscribeStreams ist das Gegenstück dazu.

Der Teil, den niemand umgehen konnte

Das ist der Punkt, der am schwersten wog, und keine der beiden Meldungen hat ihn vollständig erfasst.

Jede Börsen-API merkt sich die abonnierten Kanäle, um sie nach einem Reconnect wiederherstellen zu können. Diese Wiederherstellung sendete einen Frame pro Stream, direkt hintereinander, ganz ohne Taktung, auf eine Verbindung, die wenige Millisekunden alt war. Bei einer Watchlist realistischer Größe wurde die frische Verbindung sofort geschlossen. Der WatchDog verband sich daraufhin neu, wiederholte denselben Schwall und wurde erneut getrennt. Aus einer einzigen kurzzeitigen Trennung wurde eine dauerhafte Reconnect-Schleife.

Da dieser Schwall innerhalb der Komponente entsteht, konnte eine Anwendung ihn nicht vermeiden. Eigene Frames statt der Helfer zu schreiben half nicht, denn die Wiederherstellung ist nichts, was Sie selbst aufrufen.

Eine Prüfung des restlichen Codes zeigte, dass dieselbe Wiederherstellungsschleife in 17 Börsen-APIs kopiert worden war: Binance, Bitfinex, Bitget, Bitmex, Bitstamp, Bybit, Cex, CexPlus, Coinbase, CryptoCom, Deribit, GateIO, Huobi, Kraken, Kucoin, MEXC und OKX. Jede davon gegen das Limit ihrer eigenen Börse.

Es gibt jetzt eine Throttle-Option in den WebSocket-API-Clients:

oBinance.Throttle.Enabled := True;      // paces your own Subscribe calls
oBinance.Throttle.MaxMessages := 4;     // default, leaves room for PING/PONG
oBinance.Throttle.IntervalMs := 1000;   // default

Die Voreinstellung verdient eine Erklärung, denn sie ist bewusst asymmetrisch. Throttle.Enabled ist False, sodass sich das Timing der Aufrufe Ihrer Anwendung nicht ändert, solange Sie es nicht verlangen. Aber Throttle.PaceResubscribe ist True, sodass die Wiederherstellung nach dem Reconnect von Haus aus getaktet wird.

Die Begründung ist einfach. Ihre eigene Subscribe-Schleife haben Sie unter Kontrolle und können sie bündeln, die Taktung dort ist also Ihre Entscheidung. Die Wiederherstellung haben Sie nicht unter Kontrolle, deshalb wird sie für Sie getaktet. Damit ist die Reconnect-Schleife in jeder bestehenden Anwendung ohne jede Codeänderung behoben. Bei Binance wird die Wiederherstellung zudem als kombinierte Frames gesendet, sodass eine Watchlist mit zwölf Streams jetzt als ein einziger Frame statt als zwölf wiederhergestellt wird. Setzen Sie PaceResubscribe auf False, wenn Sie das bisherige Verhalten möchten.

Ein Detail, wenn Sie mehrere Börsen nutzen: Das voreingestellte Budget von 4 Nachrichten pro Sekunde passt zu Binance, dessen dokumentiertes Limit bei 5 liegt. OKX dokumentiert 3, deshalb steht der OKX-Client standardmäßig auf 3. Prüfen Sie das Limit Ihrer Börse, bevor Sie MaxMessages erhöhen.

Aktualisierung

Ohne Anpassungen. Die neue Exception leitet sich von der zuvor ausgelösten ab, die Batching-Methoden sind Ergänzungen, und Throttle ist standardmäßig ausgeschaltet, abgesehen von der Wiederherstellung, die die Reconnects kaputtgemacht hat. Laden Sie die neueste Version auf der sgcWebSockets Download-Seite herunter.

Beide Hinweise kamen von Kunden, die sich die Zeit genommen haben, den Quellcode zu lesen, das Problem zu reproduzieren und es präzise aufzuschreiben. So wurde der dritte und schwerwiegendste Fehler gefunden. Wenn sich etwas in den Komponenten auf eine Weise verhält, die Sie sich nicht erklären können, sagen Sie uns Bescheid.

Fragen, Feedback oder Hilfe beim Umstieg? Kontaktieren Sie uns, Sie erhalten eine Antwort von den Leuten, die den Code geschrieben haben.