Net-Base Magazine

06.06.2026

High-performance REST-server in Delphi: requestlimieten, thread-pool en correct overloadgedrag (broncodefragment)

Een high-performance REST-server in Delphi wordt niet alleen snel door 'snel JSON', maar door gecontroleerde paralleliteit, strikte time-outs en degelijk gedrag bij overbelasting. Dit artikel toont een praktisch toepasbaar concurrency-gate met semafoor en 429/503-antwoorden...

06.06.2026

Van magazinethema naar projectpraktijk

Relevante dienst- en technische pagina's bij het artikel

Waarom „High Performance“ bij REST in Delphi vaak faalt door paralleliteit

Een High Performance REST Server Delphi is in de praktijk zelden beperkt door louter CPU-tijd per request, maar door ongecontroleerde paralleliteit: te veel gelijktijdige Requests, te veel gelijktijdige database-Queries of blokkerende I/O (bestand, netwerk, database). Het resultaat voelt dan niet als „een beetje langzamer“, maar als een kettingreactie: meer Threads, meer wachtrijen, Connection-Pool-Kollaps, stijgende Latenzen, Timeouts aan clientzijde en uiteindelijk een server die weliswaar nog „leeft“, maar geen stabiele antwoorden meer levert.

Het tegengif is geen enkele truc, maar een bewust Overload-Verhalten: wanneer de server zijn grenzen bereikt, moet hij vroeg en deterministisch weigeren (typisch HTTP 429 of 503), in plaats van Requests in een oneindige wachtrij te laten lopen. Juist daarvoor is dit Source-Schnipsel bedoeld: een lichtgewicht Concurrency-Gate (Semaphore) plus Timeouts, dat zich in bestaande REST-Endpoints laat integreren – onafhankelijk of u Indy, WebBroker, Horse of een eigen HTTP-Schicht gebruikt.

Architectuuridee: Concurrency-Gate voor het „teuren deel“

Het basisidee is simpel: voor het teuren deel (Datenbankzugriff, complexe Reports, grote JSON-Antwoorden) wordt een token uit een Semaphore gereserveerd. Is er geen token vrij, dan is er meteen een gecontroleerd antwoord. Belangrijk is: deze Gate moet zuverlässig vrijgegeven worden (try/finally), en het moet in het Codepfad zitten dat werkelijk duur is – niet alleen helemaal aan het begin van de Request-Handlers, als daarna toch nog Parser/Router/Authentifizierung volgt.

Daardoor wordt Last niet „wegoptimiert“, maar gekanaliseerd: de server beantwoordt minder Requests tegelijk, maar met stabielere Latenzen. In individuele Unternehmensanwendungen is dat meestal waardevoller dan sporadische Bestzeiten in synthetische Benchmarks.

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

De volgende Delphi-Code implementeert een Concurrency-Gate als klasse TRestRequestGate. Het is gebaseerd op TSemaphore (aus System.SyncObjs; een Semaphore is een teller voor begrenzte gelijktijdige Zugriffe). De Gate-Aufruf levert ofwel een „Lease“-Objekt (RAII-ähnlich: Freigabe im Destructor) of besluit zich voor een onmiddellijke Überlast-Antwort. Daarnaast zijn er Hooks voor Logging/Monitoring, zodat Sie im Betrieb zien kunnen, warum Requests abgewiesen wurden.

Delphi
unit RESTRequestGate;

interface

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

type
  // Minimale context voor logging/tracing; kan bijv. uitgebreid worden met gebruiker/route.
  TRESTGateContext = record
    RequestId: string;
    Route: string;
    RemoteIp: string;
  end;

  TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);

  // Hook voor bedrijfstelemetrie (bijv. naar bestand, syslog, Prometheus-exporter, enz.)
  TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
                                         Decision: TRESTOverloadDecision;
                                         WaitedMs: Integer;
                                         InFlight: Integer);

  // Lease-object: vrijgave van het token in de 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: geen wachttijd, onmiddellijk 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;

  // Eerst teller verlagen, daarna semaphore vrijgeven.
  TInterlocked.Decrement(FInFlightCounter^);
  FSemaphore.Release;
end;

{ TRESTRequestGate }

constructor TRESTRequestGate.Create(AMaxInFlight: Integer);
begin
  inherited Create;
  if AMaxInFlight <= 0 then
    raise EArgumentException.Create('AMaxInFlight moet > 0 zijn');

  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 bij TimeoutMs > 0: gericht wachten, maar begrenzen.
        Decision := odRejectedTimeout;
        Result := False;
        if Assigned(FOnEvent) then
          FOnEvent(Ctx, Decision, WaitedMs, InFlight);
      end;
  else
    begin
      // wrAbandoned/foutgevallen: conservatief weigeren
      Decision := odRejectedBusy;
      Result := False;
      if Assigned(FOnEvent) then
        FOnEvent(Ctx, Decision, WaitedMs, InFlight);
    end;
  end;
end;

end.

Doel: stabiliteit bij belasting in plaats van „alles tegelijk“

Met MaxInFlight definieert u hoeveel requests gelijktijdig in het „duurdere deel“ mogen. Dat is bewust geen „aantal CPU-cores“, maar een operationele maatstaf. Bij database-intensieve endpoints is het vaak zinvol om MaxInFlight in relatie tot de DB-connection-pool te zetten (bijvoorbeeld Pool = 20, MaxInFlight = 12 tot 16), zodat niet elke request een verbinding blokkeert en vervolgens extra threads volgen.

Randvoorwaarden en valkuilen

  • Try/Finally is verplicht: De Lease moet gegarandeerd vrijgegeven worden. Als u exceptions in de endpoint heeft, wordt anders het gate „lek“ en blijft de server permanent „busy“.
  • Kies een zinvolle timeout: TimeoutMs=0 is een harde grens (direct afwijzen). Een korte timeout (typisch 50 tot 150 ms) dempt pieken zonder echte wachtrijen op te bouwen.
  • Gate niet te vroeg: Authenticatie (bijvoorbeeld Bearer/JWT) of routing kan goedkoop zijn; de semafoor moet grijpen vóór het werkelijk dure gedeelte. Omgekeerd: als auth duur wordt (bijv. tegen een extern identity-systeem), moet ook dat beperkt worden.
  • 429 vs 503: HTTP 429 („Too Many Requests“) past goed als clients gericht moeten retryen. 503 („Service Unavailable“) past wanneer de dienst tijdelijk in het algemeen niet in staat is om aanvragen zinvol te accepteren. In beide gevallen is een Retry-After-header aan te raden.

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

De snippet is bewust framework-neutraal. U heeft alleen een plek nodig waar requests „doorlopen“. Gebruikelijk is een globaal singleton of een gate per route-groep (bijvoorbeeld „/reports“ kleiner, „/health“ zonder gate). Hieronder een opname als voorbeeld:

  • Context vullen (RequestId, Route, RemoteIp)
  • TryAcquire met korte timeout
  • Bij afwijzing direct response schrijven (429/503) en beëindigen
  • De Lease leeft in de scope tot na het dure gedeelte

In Horse (middleware) zit het gate dicht bij een route-groep. In WebBroker kunt u in de betreffende action-handler werken. Bij Indy hangt het ervan af of u per request een thread heeft; het gate werkt desalniettemin zolang de dure secties netjes begrensd zijn.

High Performance REST Server Delphi: Overload-antwoorden die clients niet „vergiftigen“

Overload-antwoorden zijn meer dan statuscodes. Als clients bij 429/503 agressief direct opnieuw verzenden, krijgt u een retry-storm. In heterogene systeemlandschappen (mobile apps, C# Services, legacy-clients) helpt consistent gedrag:

  • Retry-After: bijvoorbeeld 1 tot 3 seconden, afhankelijk van het endpoint. Dat is een duidelijke timing.
  • Korte body: Een kleine JSON zoals {"error":"server_busy","requestId":"..."} is voldoende. Grote error-objecten kosten opnieuw CPU en bandbreedte.
  • Health-endpoint niet gedrosseld: Monitoring moet ook bij belasting nog uitspraken kunnen doen (bijv. met een „degraded“-flag).

Als u een reverse proxy zoals nginx ervoor gebruikt: stem daar timeouts en buffering op af. Een proxy kan ontlasten (TLS-Termination, Keep-Alive), maar ook last verplaatsen (bijvoorbeeld grote request-bodies bufferen). In de operatie telt dat de limieten consistent zijn: Proxy-Timeout > App-Timeout, anders zien clients „Gateway Timeout“, terwijl de app correct had afgewezen.

Threading, DB-Pools und Keep-Alive: Waar het in de praktijk kantelt

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-vervanging met native aansluiting-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- en 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 (globale oorzaakanalyse, zonder de gegevensbescherming te negeren)

Daardoor krijgt u een signaal of limits te streng zijn (te veel 429) of te soepel (hoge WaitedMs, stijgende latenties). En u ziet of afzonderlijke routes domineren. Voor Windows- en Linux-Services is dat in de dagelijkse praktijk cruciaal: zonder telemetrie wordt een performanceprobleem snel een gokspel tussen netwerk, database, proxy en applicatie.

Ongebruikelijk, maar extreem nuttig: „WaitedMs“ als vroegsignaal

Veel teams kijken alleen naar responsetijd en CPU. WaitedMs is vaak de betere indicator, omdat het toont dat requests al voor het eigenlijke werk wachten. Stijgt WaitedMs terwijl de CPU gematigd blijft, dan is de schaarse resource vaak niet de CPU maar een pool (DB-verbindingen), een lock in de bedrijfslogica of een externe downstream-service. Dat bespaart tijd bij de oorzaakanalyse, omdat u gerichter zoekt richting „Pool/Lock/I/O“ in plaats van „compileroptimalisatie“.

Varianten: per-route-gates, prioriteiten en „Fast Lane“

Eén gate voor alles is simpel, maar niet altijd ideaal. Zinnige varianten:

  • Gate per route-groep: „/reports“ streng, „/api/orders“ gematigd, „/health“ open. Zo voorkomt u dat zware rapportaanvragen kernprocessen verdringen.
  • Fast Lane voor Admin/Monitoring: apart gate met lage paralleliteit, zodat beheerhandelingen ook bij belasting mogelijk blijven.
  • Budget-gebaseerde limits: wanneer responsgroottes sterk variëren kan aanvullend een byte-budget helpen (bijv. maximaal X MB gelijktijdig in de generatie). Dat is complexer, maar realistisch bij grote downloads.

Belangrijk: prioritering wordt snel politiek („mijn endpoint is belangrijker“). Technisch stabiel blijft het wanneer prioriteiten aan processen gekoppeld zijn (bijv. opdrachtinname vóór reporting), niet aan rollen of afdelingen.

Conclusie: loont het Gate – en wanneer kantelt de aanpak?

Een Concurrency-Gate is een pragmatisch bouwblok voor een High Performance REST Server in Delphi, omdat het overload beheersbaar maakt en uw systemen bij piekbelasting stabiel houdt. Het betaalt zich vooral uit als u databasegebonden endpoints heeft, als er een Reverse Proxy voor zit of wanneer meerdere clients (legacy, portals, services) in golven belasting veroorzaken.

De grenzen zijn helder: als het daadwerkelijke werk per request te duur is (inefficiënte queries, grote JSON-objecten, blokkerende externe systemen), maskeert het Gate slechts symptomen. Dan moeten dataaccess, cachingstrategieën, timeouts en eventueel asynchrone verwerking (Queue/Job-System) worden aangepakt. Als veiligheidsmechanisme in de operatie is het Gate echter vaak het verschil tussen „even traag“ en „volledig onbruikbaar“.

Als u overload-gedrag in een bestaande Delphi REST-API und REST-Server wilt introduceren of limits met database- en proxy-timeouts netjes wilt afstemmen: bespreek een project of moderniseringsproject met Net-Base.

In het vakgebied spelen ook thread-pool Delphi en Http 429 Too Many Requests een belangrijke rol, wanneer integraties, datastromen en doorontwikkeling goed moeten samenwerken.

Project of moderniseringsproject met Net-Base bespreken.

Volgende stap

Wanneer het onderwerp een echt project wordt, zouden architectuur, bestaande omgeving en beheer in een vroeg stadium gezamenlijk moeten worden bekeken.

We ondersteunen niet alleen bij individuele vragen, maar ook wanneer uit broncodefragmenten, legacy-onderwerpen of portalideeën een robuust bedrijfsproject moet ontstaan.

  • Huidige situatie, doelbeeld en technische risico's worden gezamenlijk beoordeeld.
  • REST, gegevens‑toegang, portalen en uitrol worden niet als latere gevolgen uitgesteld.
  • U ziet vroeg welke weg economisch en operationeel houdbaar is.

Bericht delen

Dit bericht direct delen

LinkedIn, X, XING, Facebook, WhatsApp en e-mail zijn direct beschikbaar. Voor Instagram bereiden we de link en een korte tekst direct voor.

E-mail

Instagram opent in een nieuw tabblad. Link en korte tekst worden van tevoren naar het klembord gekopieerd.