Net-Base Magasin

06.06.2026

Højtydende REST-server i Delphi: forespørgselsgrænser, thread-pool og kontrolleret overbelastningsadfærd (kodeudsnit)

En High Performance REST-server i Delphi er ikke kun hurtig på grund af 'hurtigt JSON', men gennem kontrolleret parallelitet, stramme timeouts og klart defineret overload-adfærd. Dette indlæg viser et praksisegnet Concurrency-Gate med semafor og 429/503-svar.

06.06.2026

Fra magasinets tema til projektpraksis

Passende service- og tekniske sider til artiklen

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

Ein High Performance REST Server Delphi er i praksis sjældent begrænset af ren CPU-tid pr. request, men af ukontrolleret parallelitet: for mange samtidige requests, for mange samtidige database-queries eller blokerende I/O (fil, netværk, database). Resultatet opfører sig ikke som „lidt langsommere“, men som en kædereaktion: flere Threads, flere ventekøer, Connection-Pool-Kollaps, stigende latenser, Timeouts på klientsiden og til sidst en server, der stadig „lever“, men ikke længere leverer stabile svar.

Modmidlet er ikke et enkelt trick, men en bevidst Overload-Verhalten: Når serveren når sine grænser, skal den tidligt og deterministisk afvise (typisk HTTP 429 eller 503) i stedet for at lade requests løbe ind i en uendelig ventekø. Netop til det formål er dette source-snip beregnet: et letvægts Concurrency-Gate (Semaphore) plus timeouts, som kan integreres i eksisterende REST-endpoints – uanset om I bruger Indy, WebBroker, Horse eller et eget HTTP-lag.

Architekturidee: Concurrency-Gate vor dem „teuren Teil“

Grundideen er enkel: For den dyre del (databaseadgang, komplekse rapporter, store JSON-svar) reserveres et token fra en Semaphore. Er der intet token ledigt, gives der med det samme et kontrolleret svar. Vigtigt er: Dette gate skal pålideligt frigives (try/finally), og det skal være i den kodevej, der faktisk er dyr – ikke kun helt i starten af request-handleren, når der efterfølgende alligevel kommer parser/router/autentificering.

Dermed bliver belastningen ikke „optimeret væk“, men kanaliseret: Serveren besvarer færre requests samtidigt, men med mere stabile latenser. I individuelle virksomhedsapplikationer er det som regel mere værdifuldt end sporadiske toppræstationer i syntetiske benchmarks.

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

Følgende Delphi-kode implementerer et Concurrency-Gate som klassen TRestRequestGate. Det er baseret på TSemaphore (fra System.SyncObjs; en Semaphore er en tæller for begrænsede samtidige adgang). Gate-opkaldet leverer enten et „Lease“-objekt (RAII-ähnlich: frigivelse i Destructor) eller beslutter en øjeblikkelig overbelastningsrespons. Der er desuden hooks til logging/monitoring, så I i drift kan se, hvorfor requests blev afvist.

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 skal være > 0');

  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.

Formål: Stabilitet under belastning i stedet for „alt på én gang“

Med MaxInFlight definerer man, hvor mange Requests samtidig må gå ind i den „dyre del“. Det er bevidst ikke „antal CPU-kerner“, men en driftsstørrelse. For database-tunge Endpoints er det ofte fornuftigt at sætte MaxInFlight i relation til DB-Connection-Poolen (for eksempel Pool = 20, MaxInFlight = 12 til 16), så ikke hver Request blokerer en forbindelse og derefter trækker yderligere threads efter sig.

Randbetingelser og faldgruber

  • Try/Finally er obligatorisk: Leasen skal garanteret frigives. Hvis der opstår Exceptions i Endpointet, bliver ellers porten „utæt“ og serveren forbliver permanent „busy“.
  • Vælg timeout fornuftigt: TimeoutMs=0 er en hård grænse (afvises med det samme). En kort timeout (typisk 50 til 150 ms) udjævner peaks uden at opbygge egentlige køer.
  • Porten ikke for tidligt: Authentifizierung (for eksempel Bearer/JWT) eller routing kan være hensigtsmæssigt; semaforen bør gribe ind før den reelt dyre sektion. Omvendt: Hvis auth bliver dyr (f.eks. mod et eksternt Identity-System), skal også det begrænses.
  • 429 vs 503: HTTP 429 („Too Many Requests“) passer godt, når klienter skal foretage målrettede retries. 503 („Service Unavailable“) passer, når tjenesten midlertidigt generelt ikke er i stand til at modtage forespørgsler meningsfuldt. I begge tilfælde er en Retry-After-Header anbefalelsesværdig.

Integration i REST-Handler: Indy/WebBroker/Horse pragmatisk

Snippetet er bevidst framework-neutralt. Man behøver kun et sted, hvor Requests „løber igennem“. Typisk er et globalt Singleton eller en Gate per route-gruppe (for eksempel „/reports“ med mindre gate, „/health“ uden gate). Eksempelvis indbindingen som mønster:

  • Fyld kontekst (RequestId, Route, RemoteIp)
  • TryAcquire med kort timeout
  • Ved afvisning skriv straks Response (429/503) og afslut
  • Leasen lever i scope indtil efter den dyre del

I Horse (Middleware) ligger gaten tæt på en route-gruppe. I WebBroker kan man arbejde i den respektive Action-Handler. For Indy afhænger det af, om man har en tråd per Request; gaten virker alligevel, så længe de dyre sektioner er klart afgrænsede.

High Performance REST Server Delphi: Overbelastningssvar, der ikke „forgifte“ klienter

Overbelastningssvar er mere end statuskoder. Hvis klienter ved 429/503 aggressivt sender igen med det samme, får man en retry-storm. I heterogene systemlandskaber (Mobile Apps, C# Services, Legacy-Clients) hjælper en konsistent adfærd:

  • Retry-After: for eksempel 1 til 3 sekunder, afhængig af Endpoint. Det er en klar taktgiver.
  • Kort Body: Et lille JSON som {"error":"server_busy","requestId":"..."} er tilstrækkeligt. Store Error-objekter koster igen CPU og båndbredde.
  • Health-Endpoint uden begrænsning: Monitoring skal også under belastning kunne give information (evt. med „degraded“-Flag).

Hvis man kører en Reverse Proxy som nginx foran: afstem Timeouts og Buffering der. En Proxy kan aflaste (TLS-Termination, Keep-Alive), men også flytte belastning (for eksempel buffre store Request-Bodies). I drift er det afgørende, at grænserne er konsistente: Proxy-Timeout > App-Timeout, ellers ser klienter „Gateway Timeout“, selvom appen korrekt ville have afvist forespørgslen.

Threading, DB-pools und Keep-Alive: Hvor det går galt i praksis

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:

  • En Request blokerer flere knappe Ressourcen: Først en DB-forbindelse, så et eksternt HTTP-kald, så en filadgang. Hvis det hele sker i samme request-tråd, multipliceres blokeringstiden. Das Gate begrenzt dann zwar die Parallelität, aber die Durchsatzleistung sinkt drastisch. Her 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 (overordnet årsagsanalyse, uden at ignorere databeskyttelse)

Dermed får I et signal om, hvorvidt limits er for stramme (for mange 429) eller for lempelige (høje WaitedMs, stigende latenser). Og I ser, om enkelte ruter dominerer. For Windows- og Linux-Services er det i praksis afgørende: Uden telemetri bliver et performanceproblem hurtigt et gætteri mellem netværk, database, proxy og applikation.

Usædvanligt, men yderst nyttigt: „WaitedMs“ som tidlig advarselsindikator

Mange teams kigger kun på response-time og CPU. WaitedMs er ofte den bedre indikator, fordi den viser, at requests allerede før det egentlige arbejde venter. Hvis WaitedMs stiger, mens CPU’en forbliver moderat, er den knappe ressource ofte ikke CPU’en, men en pool (DB-forbindelser), et lock i forretningslogikken eller en ekstern downstream-service. Det sparer tid i årsagsanalysen, fordi I kan søge mere målrettet mod „Pool/Lock/I/O“ i stedet for „Compiler-Optimierung“.

Varianter: Pro-Route-Gates, prioriteter og „Fast Lane“

Et gate for alt er enkelt, men ikke altid ideelt. Fornuftige varianter:

  • Gate pr. route-gruppe: „/reports“ strengt, „/api/orders“ moderat, „/health“ åbent. Så forhindrer I, at tunge report-requests trænges ind foran kerneprocesser.
  • Fast Lane for Admin/Monitoring: Separat gate med lav parallelitet, så driftshandlinger også er mulige under belastning.
  • Budget-baserede limits: Når response-størrelser varierer meget, kan et ekstra byte-budget hjælpe (fx maksimalt X MB samtidig under generering). Det er mere komplekst, men realistisk ved store downloads.

Vigtigt: Prioritering bliver hurtigt politisk („min endpoint er vigtigere“). Teknisk stabilt forbliver det, når prioriteter er koblet til processer (fx ordreindføring før rapportering), ikke til roller eller afdelinger.

Konklusion: Betaler Gate’et sig – og hvornår svigter tilgangen?

Et Concurrency-Gate er en pragmatisk byggesten for en High Performance REST Server i Delphi, fordi det gør overload kontrollerbart og holder jeres systemer stabile ved peak-belastning. Det er særligt relevant, hvis I har databasebundne endpoints, en reverse proxy foran eller flere klienter (legacy, portaler, services), der genererer belastning i bølger.

Grænserne er klare: Hvis det egentlige arbejde per request er for dyrt (ineffektive queries, store JSON-objekter, blokerende eksterne systemer), skjuler gattet kun symptomer. Så må dataadgang, caching-strategier, timeouts og eventuelt asynkron behandling (Queue/Job-System) forbedres. Som sikkerhedssele i drift er gattet dog ofte forskellen mellem «lidt langsomt» og «fuldstændig ubrugeligt».

Hvis I vil indføre overload-adfærd i en eksisterende Delphi REST-API und REST-Server eller afstemme limits med database- og proxy-timeouts: Drøft projekt eller moderniseringsforløb med Net-Base.

I det faglige miljø spiller også Thread-Pool Delphi og Http 429 Too Many Requests en vigtig rolle, når integrationer, dataflows og videreudvikling skal spille rent sammen.

Projekt eller Modernisierungsvorhaben mit Net-Base besprechen.

Næste trin

Når et emne bliver til et reelt projekt, bør arkitektur, eksisterende systemer og drift tidligt vurderes samlet.

Vi støtter ikke kun ved enkeltspørsmål, men også når kildekodeudsnit, legacy-komponenter eller portalidéer skal udvikles til et robust virksomhedsprojekt.

  • Eksisterende tilstand, målbillede og tekniske risici vurderes samlet.
  • REST, dataadgang, portaler og idrulning bliver ikke udskudt som eftertanker.
  • I ser tidligt, hvilken vej der er økonomisk og driftsmæssigt holdbar.

Del indlæg

Del dette indlæg direkte

LinkedIn, X, XING, Facebook, WhatsApp og e-mail er straks tilgængelige. Til Instagram forbereder vi link og kort tekst med det samme.

E-mail

Instagram åbner i en ny fane. Linket og kortteksten kopieres på forhånd til udklipsholderen.