Net-Base Magazin

06.06.2026

High Performance REST Server in Delphi: Request-Limits, Thread-Pool und sauberes Overload-Verhalten (Source-Schnipsel)

Ein High Performance REST Server in Delphi wird nicht nur durch „schnelles JSON“ schnell, sondern durch kontrollierte Parallelität, harte Timeouts und sauberes Overload-Verhalten. Dieser Beitrag zeigt ein praxistaugliches Concurrency-Gate mit Semaphore, 429/503-Antworten...

06.06.2026

Vom Magazinthema zur Projektpraxis

Passende Leistungs- und Technikseiten zum Beitrag

Warum „High Performance“ bei REST in Delphi oft an Parallelität scheitert

Ein High Performance REST Server Delphi ist in der Praxis selten durch reine CPU-Zeit pro Request limitiert, sondern durch unkontrollierte Parallelität: zu viele gleichzeitige Requests, zu viele gleichzeitige Datenbank-Queries oder blockierende I/O (Datei, Netzwerk, Datenbank). Das Ergebnis wirkt dann nicht wie „ein bisschen langsamer“, sondern wie eine Kettenreaktion: mehr Threads, mehr Warteschlangen, Connection-Pool-Kollaps, steigende Latenzen, Timeouts auf Client-Seite und am Ende ein Server, der zwar noch „lebt“, aber keine stabilen Antworten mehr liefert.

Das Gegenmittel ist kein einzelner Trick, sondern ein bewusstes Overload-Verhalten: Wenn der Server an seine Grenzen kommt, muss er früh und deterministisch zurückweisen (typisch HTTP 429 oder 503), statt Requests in eine unendliche Warteschlange laufen zu lassen. Genau dafür ist dieser Source-Schnipsel gedacht: ein leichtgewichtiges Concurrency-Gate (Semaphore) plus Timeouts, das sich in bestehende REST-Endpoints integrieren lässt – unabhängig davon, ob Sie Indy, WebBroker, Horse oder eine eigene HTTP-Schicht einsetzen.

Architekturidee: Concurrency-Gate vor dem „teuren Teil“

Die Grundidee ist simpel: Vor dem teuren Teil (Datenbankzugriff, komplexe Reports, große JSON-Antworten) wird ein Token aus einer Semaphore reserviert. Ist kein Token frei, gibt es sofort eine kontrollierte Antwort. Wichtig ist: Dieses Gate muss zuverlässig freigegeben werden (try/finally), und es muss in den Codepfad, der wirklich teuer ist – nicht nur ganz am Anfang des Request-Handlers, wenn danach ohnehin noch Parser/Router/Authentifizierung kommt.

Damit wird Last nicht „wegoptimiert“, sondern kanalisiert: Der Server beantwortet weniger Requests gleichzeitig, dafür mit stabileren Latenzen. In individuellen Unternehmensanwendungen ist das meistens wertvoller als sporadische Bestzeiten in synthetischen Benchmarks.

Source-Schnipsel: Request-Limiter mit Timeout, 429/503 und Telemetrie-Hooks

Der folgende Delphi-Code implementiert ein Concurrency-Gate als Klasse TRestRequestGate. Es basiert auf TSemaphore (aus System.SyncObjs; eine Semaphore ist ein Zähler für begrenzte gleichzeitige Zugriffe). Der Gate-Aufruf liefert entweder ein „Lease“-Objekt (RAII-ähnlich: Freigabe im Destructor) oder entscheidet sich für eine sofortige Überlast-Antwort. Zusätzlich gibt es Hooks für Logging/Monitoring, damit Sie im Betrieb sehen, warum Requests abgewiesen wurden.

Delphi
unit RestRequestGate;

interface

uses
  System.SysUtils,
  System.Classes,
  System.SyncObjs,
  System.Diagnostics;

type
  // Minimaler Kontext für Logging/Tracing; kann z.B. um User/Route erweitert werden.
  TRestGateContext = record
    RequestId: string;
    Route: string;
    RemoteIp: string;
  end;

  TRestOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);

  // Hook für Betriebstelemetrie (z.B. in Datei, Syslog, Prometheus-Exporter, etc.)
  TRestGateEvent = reference to procedure(const Ctx: TRestGateContext;
                                         Decision: TRestOverloadDecision;
                                         WaitedMs: Integer;
                                         InFlight: Integer);

  // Lease-Objekt: Freigabe des Tokens im Destructor.
  TRestGateLease = class
  private
    FSemaphore: TSemaphore;
    FInFlightCounter: PInteger;
    FReleased: Boolean;
  public
    constructor Create(ASem: TSemaphore; ACounter: PInteger);
    destructor Destroy; override;
    procedure Release;
  end;

  TRestRequestGate = class
  private
    FSem: TSemaphore;
    FMaxInFlight: Integer;
    FInFlight: Integer;
    FOnEvent: TRestGateEvent;
  public
    constructor Create(AMaxInFlight: Integer);
    destructor Destroy; override;

    // TimeoutMs = 0: keine Wartezeit, sofort 429/503
    function TryAcquire(const Ctx: TRestGateContext; TimeoutMs: Cardinal;
                        out Lease: TRestGateLease;
                        out WaitedMs: Integer;
                        out Decision: TRestOverloadDecision): Boolean;

    property OnEvent: TRestGateEvent read FOnEvent write FOnEvent;
    property MaxInFlight: Integer read FMaxInFlight;
    function InFlight: Integer;
  end;

implementation

uses
  System.Math;

{ TRestGateLease }

constructor TRestGateLease.Create(ASem: TSemaphore; ACounter: PInteger);
begin
  inherited Create;
  FSemaphore := ASem;
  FInFlightCounter := ACounter;
  FReleased := False;
end;

destructor TRestGateLease.Destroy;
begin
  Release;
  inherited;
end;

procedure TRestGateLease.Release;
begin
  if FReleased then
    Exit;
  FReleased := True;

  // Erst Counter runter, dann Semaphore freigeben.
  TInterlocked.Decrement(FInFlightCounter^);
  FSemaphore.Release;
end;

{ TRestRequestGate }

constructor TRestRequestGate.Create(AMaxInFlight: Integer);
begin
  inherited Create;
  if AMaxInFlight <= 0 then
    raise EArgumentException.Create('AMaxInFlight muss > 0 sein');

  FMaxInFlight := AMaxInFlight;
  FInFlight := 0;

  // InitialCount = MaxCount = AMaxInFlight
  FSem := TSemaphore.Create(nil, AMaxInFlight, AMaxInFlight, '');
end;

destructor TRestRequestGate.Destroy;
begin
  FSem.Free;
  inherited;
end;

function TRestRequestGate.InFlight: Integer;
begin
  Result := TInterlocked.CompareExchange(FInFlight, 0, 0);
end;

function TRestRequestGate.TryAcquire(const Ctx: TRestGateContext; TimeoutMs: Cardinal;
  out Lease: TRestGateLease; out WaitedMs: Integer; out Decision: TRestOverloadDecision): Boolean;
var
  Sw: TStopwatch;
  WaitRes: TWaitResult;
  CurrentInFlight: Integer;
begin
  Lease := nil;
  WaitedMs := 0;
  Decision := odRejectedBusy;

  Sw := TStopwatch.StartNew;
  if TimeoutMs = 0 then
    WaitRes := FSem.WaitFor(0)
  else
    WaitRes := FSem.WaitFor(TimeoutMs);

  WaitedMs := Integer(Min(Sw.ElapsedMilliseconds, High(Integer)));

  case WaitRes of
    wrSignaled:
      begin
        CurrentInFlight := TInterlocked.Increment(FInFlight);
        Lease := TRestGateLease.Create(FSem, @FInFlight);
        Decision := odAccepted;
        Result := True;

        if Assigned(FOnEvent) then
          FOnEvent(Ctx, Decision, WaitedMs, CurrentInFlight);
      end;

    wrTimeout:
      begin
        // wrTimeout bei TimeoutMs > 0: gezielt warten, aber begrenzen.
        Decision := odRejectedTimeout;
        Result := False;
        if Assigned(FOnEvent) then
          FOnEvent(Ctx, Decision, WaitedMs, InFlight);
      end;
  else
    begin
      // wrAbandoned/Fehlerfälle: konservativ zurückweisen
      Decision := odRejectedBusy;
      Result := False;
      if Assigned(FOnEvent) then
        FOnEvent(Ctx, Decision, WaitedMs, InFlight);
    end;
  end;
end;

end.

Zweck: Stabilität unter Last statt „alles gleichzeitig“

Mit MaxInFlight definieren Sie, wie viele Requests gleichzeitig in den „teuren Teil“ dürfen. Das ist bewusst nicht „Anzahl CPU-Kerne“, sondern eine Betriebsgröße. Bei datenbanklastigen Endpoints ist es häufig sinnvoll, MaxInFlight in Relation zum DB-Connection-Pool zu setzen (zum Beispiel Pool = 20, MaxInFlight = 12 bis 16), damit nicht jeder Request eine Verbindung blockiert und dann weitere Threads nachziehen.

Randbedingungen und Stolperfallen

  • Try/Finally ist Pflicht: Der Lease muss garantiert freigegeben werden. Wenn Sie Exceptions im Endpoint haben, wird sonst das Gate „undicht“ und der Server bleibt dauerhaft auf „busy“.
  • Timeout sinnvoll wählen: TimeoutMs=0 ist ein hartes Limit (sofort abweisen). Ein kurzes Timeout (typisch 50 bis 150 ms) glättet Peaks, ohne echte Warteschlangen aufzubauen.
  • Gate nicht zu früh: Authentifizierung (zum Beispiel Bearer/JWT) oder Routing kann günstig sein; die Semaphore sollte vor dem wirklich teuren Abschnitt greifen. Umgekehrt: Wenn Auth teuer wird (z.B. gegen ein externes Identity-System), muss auch das begrenzt werden.
  • 429 vs 503: HTTP 429 („Too Many Requests“) passt gut, wenn Clients gezielt retryen sollen. 503 („Service Unavailable“) passt, wenn der Dienst temporär generell nicht in der Lage ist, Anfragen sinnvoll anzunehmen. In beiden Fällen ist ein Retry-After-Header empfehlenswert.

Integration in REST-Handler: Indy/WebBroker/Horse pragmatisch

Der Snippet ist absichtlich framework-neutral. Sie brauchen nur einen Ort, an dem Requests „durchlaufen“. Typisch ist ein globales Singleton oder ein Gate pro Route-Gruppe (zum Beispiel „/reports“ kleiner, „/health“ ohne Gate). Beispielhaft die Einbindung als Muster:

  • Kontext füllen (RequestId, Route, RemoteIp)
  • TryAcquire mit kurzem Timeout
  • Bei Ablehnung sofort Response schreiben (429/503) und beenden
  • Lease lebt im Scope bis nach dem teuren Teil

In Horse (Middleware) liegt das Gate nahe an einer Route-Gruppe. In WebBroker können Sie im jeweiligen Action-Handler arbeiten. Bei Indy hängt es davon ab, ob Sie pro Request einen Thread haben; das Gate wirkt trotzdem, solange die teuren Abschnitte sauber begrenzt werden.

High Performance REST Server Delphi: Overload-Antworten, die Clients nicht „vergiften“

Überlast-Antworten sind mehr als Statuscodes. Wenn Clients bei 429/503 aggressiv sofort erneut senden, haben Sie einen Retry-Sturm. In heterogenen Systemlandschaften (Mobile Apps, C# Services, Legacy-Clients) hilft ein konsistentes Verhalten:

  • Retry-After: zum Beispiel 1 bis 3 Sekunden, je nach Endpoint. Das ist ein klarer Taktgeber.
  • Kurzer Body: Ein kleines JSON wie {"error":"server_busy","requestId":"..."} reicht. Große Error-Objekte kosten wieder CPU und Bandbreite.
  • Health-Endpoint ungedrosselt: Monitoring soll auch bei Last noch Aussagen liefern (ggf. mit „degraded“-Flag).

Wenn Sie einen Reverse Proxy wie nginx davor betreiben: Timeouts und Buffering dort abstimmen. Ein Proxy kann entlasten (TLS-Termination, Keep-Alive), aber auch Last verschieben (zum Beispiel große Request-Bodies puffern). Im Betrieb zählt, dass die Limits konsistent sind: Proxy-Timeout > App-Timeout, sonst sehen Clients „Gateway Timeout“, obwohl die App sauber abgewiesen hätte.

Threading, DB-Pools und Keep-Alive: Wo es in der Praxis kippt

Das Gate löst das „zu viele gleichzeitig“-Problem, aber es verhindert nicht automatisch, dass ein einzelner Request übermäßig viele Ressourcen bindet. Drei typische Kipp-Punkte aus Delphi-Projekten entstehen genau an den Schnittstellen zwischen Threading, Datenbank und HTTP-Verbindungen:

  • Ein Request blockiert mehrere knappe Ressourcen: Erst eine DB-Verbindung, dann ein externer HTTP-Call, dann ein Dateizugriff. Wenn das alles im selben Request-Thread passiert, multipliziert sich die Blockadezeit. Das Gate begrenzt dann zwar die Parallelität, aber die Durchsatzleistung sinkt drastisch. Hier lohnt es sich, die Abhängigkeiten zu entkoppeln (z.B. externe Calls asynchron, Vorberechnung per Job-Queue).
  • BDE-Ablosung mit nativer Anbindung-Pooling und Transaktionen: BDE-Ablosung mit nativer Anbindung kann Connections poolen, aber eine „lange“ Transaktion (z.B. weil JSON-Erstellung oder Business-Checks zwischen StartTransaction und Commit liegen) hält die Verbindung unnötig. Eine saubere Praxis ist, die Transaktion so eng wie möglich um die eigentlichen Statements zu legen und außerhalb der Transaktion zu serialisieren oder zu validieren, wenn es fachlich geht.
  • HTTP Keep-Alive als versteckter Speicherfresser: Keep-Alive reduziert Handshakes, kann aber bei vielen Leerlauf-Clients zu vielen offenen Sockets führen. Gerade bei Windows- und Linux-Services sieht man dann nicht „CPU hoch“, sondern „Handles/FDs voll“ oder RAM durch Puffer. Hier helfen klare Idle-Timeouts im Server und am Reverse Proxy sowie ein Limit pro Client-IP, wenn es die Umgebung erlaubt.

Die Konsequenz: MaxInFlight ist kein statischer Wert. Er hängt von Ihrer langsamsten, knappsten Ressource ab (DB, externe Systeme, Storage) und davon, wie gut ein Request diese Ressourcen „zusammenhält“.

Performance-Hebel neben dem Gate: JSON, DB und I/O nicht vermischen

Das Gate stabilisiert, aber es ersetzt keine saubere Endpoint-Ökonomie. Drei Bremsen in Delphi REST-Servern tauchen wiederholt auf:

  • JSON-Building mit unnötigen Zwischenstrings: Häufig entsteht Last durch viele temporäre Unicode-Strings. Wo möglich, streaming-orientiert bauen (Writer/Stream) statt riesige Zwischenobjekte, besonders bei Listen-Endpunkten.
  • Datenbankzugriff „pro Item“: N+1-Queries und per-Row Lookups sind der Klassiker. Besser: gezielte Joins, Batch-Queries, serverseitige Aggregation. Bei sehr großen Ergebnissen lohnt sich zusätzlich Pagination mit stabiler Sortierung (damit Seiten nicht „springen“).
  • Blockierende I/O im Request-Thread: Dateizugriffe oder externe HTTP-Calls sollten entweder strikt begrenzt oder in eine asynchrone Pipeline verlagert werden. Sonst blockieren Sie teure Threads für „Warten“.

Für gewachsene digitale Unternehmenslösungen ist das oft der Knackpunkt: Ein Endpoint wurde „mal schnell“ ergänzt und funktioniert, bis reale Last und Datenvolumina kommen. Dann zeigt sich, ob Architekturgrenzen sauber gezogen wurden (Datenzugriffsschicht, Caching, Bulk-Strategien, klare Timeouts).

Debugging und Betrieb: Was Sie messen sollten

Der Hook OnEvent ist bewusst simpel. In der Praxis sollten Sie mindestens folgende Werte erfassen:

  • InFlight (aktuelle Parallelität am Gate)
  • WaitedMs (wie viel „Queueing“ Sie zulassen)
  • Decision (accepted/busy/timeout)
  • Route/RemoteIp (grobe Ursachenanalyse, ohne Datenschutz zu ignorieren)

Damit bekommen Sie ein Signal, ob Limits zu streng sind (zu viele 429) oder zu weich (hohe WaitedMs, steigende Latenzen). Und Sie sehen, ob einzelne Routen dominieren. Für Windows- und Linux-Services ist das im Alltag entscheidend: Ohne Telemetrie wird ein Performanceproblem schnell zum Ratespiel zwischen Netzwerk, Datenbank, Proxy und Anwendung.

Ungewöhnlich, aber extrem hilfreich: „WaitedMs“ als Frühwarnindikator

Viele Teams schauen nur auf Response-Time und CPU. WaitedMs ist oft der bessere Indikator, weil er zeigt, dass Requests bereits vor der eigentlichen Arbeit warten. Steigt WaitedMs, während CPU moderat bleibt, ist die knappe Ressource häufig nicht die CPU, sondern ein Pool (DB-Verbindungen), ein Lock in der Business-Logik oder ein externer Downstream-Service. Das spart Zeit bei der Ursachenanalyse, weil Sie gezielter in Richtung „Pool/Lock/I/O“ statt „Compiler-Optimierung“ suchen.

Varianten: Pro-Route-Gates, Prioritäten und „Fast Lane“

Ein Gate für alles ist einfach, aber nicht immer ideal. Sinnvolle Varianten:

  • Gate pro Route-Gruppe: „/reports“ streng, „/api/orders“ moderat, „/health“ offen. So verhindern Sie, dass teure Report-Requests Kernprozesse verdrängen.
  • Fast Lane für Admin/Monitoring: Separates Gate mit kleiner Parallelität, damit Betriebshandlungen auch bei Last möglich sind.
  • Budget-basierte Limits: Wenn Response-Größen stark variieren, kann zusätzlich ein Byte-Budget helfen (z.B. maximal X MB gleichzeitig in der Generierung). Das ist komplexer, aber bei großen Downloads realistisch.

Wichtig: Priorisierung ist schnell politisch („mein Endpoint ist wichtiger“). Technisch stabil bleibt es, wenn Prioritäten an Prozesse gekoppelt sind (z.B. Auftragserfassung vor Reporting), nicht an Rollen oder Abteilungen.

Fazit: Lohnt sich das Gate – und wo kippt der Ansatz?

Ein Concurrency-Gate ist ein pragmatischer Baustein für einen High Performance REST Server in Delphi, weil es Overload kontrollierbar macht und Ihre Systeme bei Peak-Last stabil hält. Es lohnt sich besonders, wenn Sie datenbankgebundene Endpoints haben, wenn ein Reverse Proxy davor steht oder wenn mehrere Clients (Legacy, Portale, Services) in Wellen Last erzeugen.

Die Grenzen sind klar: Wenn die eigentliche Arbeit pro Request zu teuer ist (ineffiziente Queries, große JSON-Objekte, blockierende Fremdsysteme), kaschiert das Gate nur Symptome. Dann müssen Datenzugriff, Caching-Strategien, Timeouts und ggf. asynchrone Verarbeitung (Queue/Job-System) nachgezogen werden. Als Sicherheitsgurt im Betrieb ist das Gate aber oft der Unterschied zwischen „kurz zäh“ und „komplett unbrauchbar“.

Wenn Sie Overload-Verhalten in einen bestehenden Delphi REST-API und REST-Server einziehen oder Limits mit Datenbank- und Proxy-Timeouts sauber ausbalancieren möchten: Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.

Im fachlichen Umfeld spielen auch Thread-Pool Delphi und Http 429 Too Many Requests eine wichtige Rolle, wenn Integrationen, Datenflüsse und Weiterentwicklung sauber zusammenspielen müssen.

Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.

Nächster Schritt

Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb frueh zusammen betrachtet werden.

Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.

  • Bestand, Zielbild und technische Risiken werden zusammen bewertet.
  • REST, Datenzugriff, Portale und Rollout werden nicht als Spaetfolgen verschoben.
  • Sie sehen frueh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.

Beitrag teilen

Diesen Beitrag direkt weitergeben

LinkedIn, X, XING, Facebook, WhatsApp und E-Mail sind sofort verfügbar. Für Instagram bereiten wir Link und Kurztext direkt vor.

E-Mail

Instagram oeffnet in einem neuen Tab. Link und Kurztext werden vorher in die Zwischenablage kopiert.