Net-Base Magasin

06.06.2026

Høyytelses REST-server i Delphi: forespørselsgrenser, trådpool og ryddig overbelastningsoppførsel (kildeeksempel)

En High Performance REST-server i Delphi blir ikke bare rask av «raskt JSON», men av kontrollert parallellitet, strenge timeouts og ryddig overbelastningsatferd. Denne artikkelen viser et praktisk Concurrency-Gate med Semaphore, 429/503-svar...

06.06.2026

Fra magasinetema til prosjektpraksis

Egnede tjeneste- og tekniske sider for innlegget

Hvorfor „High Performance“ for REST i Delphi ofte feiler på grunn av parallellitet

En High Performance REST Server Delphi er i praksis sjelden begrenset av ren CPU-tid per forespørsel, men av ukontrollert parallellitet: for mange samtidige forespørsler, for mange samtidige databaseforespørsler eller blokkert I/O (fil, nettverk, database). Resultatet føles da ikke som „litt tregere“, men som en kjedereaksjon: flere tråder, flere køer, connection-pool-kollaps, økende latenser, timeouts på klientsiden og til slutt en server som fortsatt „lever“, men som ikke leverer stabile svar.

Motmiddelet er ikke et enkelt triks, men en bevisst Overload-Verhalten: når serveren når sine grenser må den tidlig og deterministisk avvise (typisk HTTP 429 eller 503), i stedet for å la forespørsler gå inn i en uendelig kø. Dette kodeutdraget er ment for nettopp dette: et lettvekts Concurrency-Gate (Semaphore) pluss timeouts, som kan integreres i eksisterende REST-endpoints – uavhengig av om du bruker Indy, WebBroker, Horse eller et eget HTTP-lag.

Arkitekturidé: Concurrency-Gate foran den «kostbare delen»

Grunnideen er enkel: Før den kostbare delen (database-tilgang, komplekse rapporter, store JSON-svar) reserveres et token fra en semaphore. Er det ingen ledig token, returneres umiddelbart et kontrollert svar. Viktig er: Dette gate må pålitelig frigjøres (try/finally), og det må plasseres i kodebanen som virkelig er kostbar – ikke bare helt i starten av request-handleren, hvis det likevel følger parser/router/autentisering etterpå.

Slik blir belastningen ikke «optimalisert bort», men kanalisert: serveren svarer på færre forespørsler samtidig, men med mer stabile latenser. I individuelle bedriftsapplikasjoner er dette som oftest mer verdifullt enn sporadiske bestetider i syntetiske benchmarks.

Kodeutdrag: Request-Limiter med Timeout, 429/503 og Telemetri-Hooks

Følgende Delphi-kode implementerer et Concurrency-Gate som klasse TRestRequestGate. Det er basert på TSemaphore (fra System.SyncObjs; en semaphore er en teller for begrensede samtidige tilganger). Gate-kallet leverer enten et „Lease“-objekt (RAII-lignende: frigjøring i Destructor) eller avgjør seg for en umiddelbar overbelastningsrespons. I tillegg finnes hooks for logging/monitoring, slik at du i drift kan se hvorfor forespørsler ble avvist.

Delphi
unit RESTRequestGate;

interface

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

type
  // Minimal kontekst for logging/sporing; kan f.eks. utvides med bruker/rute.
  TRESTGateContext = record
    RequestId: string;
    Route: string;
    RemoteIp: string;
  end;

  TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);

  // Hook for driftstelemetri (f.eks. til fil, syslog, Prometheus-eksporter, osv.)
  TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
                                         Decision: TRESTOverloadDecision;
                                         WaitedMs: Integer;
                                         InFlight: Integer);

  // Lease-objekt: frigjøring av token i destruktøren.
  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: ingen ventetid, umiddelbart 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;

  // Først reduser telleren, deretter frigjør semaforen.
  TInterlocked.Decrement(FInFlightCounter^);
  FSemaphore.Release;
end;

{ TRESTRequestGate }

constructor TRESTRequestGate.Create(AMaxInFlight: Integer);
begin
  inherited Create;
  if AMaxInFlight <= 0 then
    raise EArgumentException.Create('AMaxInFlight må 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 når TimeoutMs > 0: målrettet venting, men begrenset.
        Decision := odRejectedTimeout;
        Result := False;
        if Assigned(FOnEvent) then
          FOnEvent(Ctx, Decision, WaitedMs, InFlight);
      end;
  else
    begin
      // wrAbandoned/feiltilfeller: avvis konservativt
      Decision := odRejectedBusy;
      Result := False;
      if Assigned(FOnEvent) then
        FOnEvent(Ctx, Decision, WaitedMs, InFlight);
    end;
  end;
end;

end.

Formål: Stabilitet under belastning fremfor «alt samtidig»

Med MaxInFlight definerer du hvor mange requests som samtidig får gå inn i den «kostbare delen». Dette er bevisst ikke «antall CPU-kjerner», men en driftsparameter. For database­tunge endepunkter er det ofte fornuftig å sette MaxInFlight i forhold til DB-connection-poolen (for eksempel Pool = 20, MaxInFlight = 12 til 16), slik at ikke hver request blokkerer en forbindelse og deretter får flere tråder til å henge etter.

Randbetingelser og fallgruver

  • Try/Finally er obligatorisk: Leasen må frigjøres garantert. Hvis du har unntak i endepunktet, blir ellers porten «lekk» og serveren forblir varig «busy».
  • Velg timeout fornuftig: TimeoutMs=0 er et hardt tak (avvises umiddelbart). En kort timeout (typisk 50 til 150 ms) jevner ut topper uten å bygge opp ekte køer.
  • Porten ikke for tidlig: Autentisering (for eksempel Bearer/JWT) eller routing kan være billig; semaforen bør treffe før den virkelig kostbare delen. Omvendt: hvis autentisering blir dyr (f.eks. mot et eksternt identity-system), må også den begrenses.
  • 429 vs 503: HTTP 429 («Too Many Requests») passer godt når klienter skal gjøre målrettede retries. 503 («Service Unavailable») passer når tjenesten midlertidig generelt ikke er i stand til å ta imot forespørsler på en meningsfull måte. I begge tilfeller anbefales en Retry-After-header.

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

Snippetet er bevisst framework-nøytralt. Du trenger bare ett sted hvor requests «gjennomløper». Typisk er et globalt singleton eller en port per rutegruppe (for eksempel «/reports» mindre, «/health» uten port). Eksemplarisk integrasjon som mønster:

  • Fyll kontekst (RequestId, Route, RemoteIp)
  • TryAcquire med kort timeout
  • Ved avslag: skriv response umiddelbart (429/503) og avslutt
  • Leasen lever i Scope til etter den kostbare delen

I Horse (Middleware) ligger porten nær en rutegruppe. I WebBroker kan du arbeide i den respektive Action-Handler. For Indy avhenger det av om du har en tråd per request; porten virker likevel så lenge de kostbare seksjonene er tydelig avgrenset.

High Performance REST Server Delphi: Overbelastningssvar som ikke «forgifter» klientene

Overlast-svar er mer enn statuskoder. Hvis klienter ved 429/503 aggressivt sender på nytt umiddelbart, får du en retry-storm. I heterogene systemlandskap (Mobile Apps, C# Services, legacy-klienter) hjelper et konsistent oppførsel:

  • Retry-After: for eksempel 1 til 3 sekunder, avhengig av endepunkt. Det er en klar taktgiver.
  • Kort body: En liten JSON som {"error":"server_busy","requestId":"..."} er tilstrekkelig. Store error-objekter koster igjen CPU og båndbredde.
  • Health-endepunkt uten throttling: Overvåkning skal også under last gi utsagn (eventuelt med «degraded»-flagg).

Hvis du kjører en reverse proxy som nginx foran: synkroniser timeouts og buffering der. En proxy kan avlaste (TLS-termination, Keep-Alive), men også flytte last (for eksempel buffre store request-bodies). I drift er det avgjørende at grensene er konsistente: Proxy-Timeout > App-Timeout, ellers ser klientene «Gateway Timeout», selv om appen korrekt ville ha avvist.

Threading, DB-pools und Keep-Alive: Hvor det svikter i praksis

Das Gate løser problemet med „zu viele gleichzeitig“, men det hindrer ikke automatisk at en enkelt Request binder for mange ressurser. Tre typiske sviktpunkter fra Delphi-prosjekter oppstår nettopp i skjæringsflatene mellom threading, database og HTTP-tilkoblinger:

  • En Request blokkerer flere knappe ressurser: Først en DB-tilkobling, så et eksternt HTTP-kall, så en filtilgang. Hvis alt dette skjer i samme Request-tråd, multipliseres blokkeringstiden. Das Gate begrenser da zwar parallelliteten, men gjennomstrømningen faller drastisk. Her lønner det seg å løsrive avhengighetene (f.eks. eksterne kall asynkront, forhåndsberegning via job-kø).
  • BDE-Ablosung mit nativer Anbindung-Pooling und Transaktionen: BDE-Ablosung mit nativer Anbindung kan pool’e tilkoblinger, men en „lang“ transaksjon (z.B. weil JSON-Erstellung oder Business-Checks zwischen StartTransaction und Commit liegen) holder forbindelsen unødvendig. En god praksis er å begrense transaksjonen så tett som mulig rundt de faktiske statements og å utføre serialisering eller validering utenfor transaksjonen, hvis det faglig lar seg gjøre.
  • HTTP Keep-Alive als versteckter Speicherfresser: Keep-Alive reduserer handshakes, kan aber bei vielen Leerlauf-Clients zu vielen offenen Sockets führen. Spesielt ved Windows- und Linux-Services ser man da ikke „CPU hoch“, sondern „Handles/FDs voll“ oder RAM durch Puffer. Her hjelper klare idle-timeouts i serveren og ved reverse-proxyen samt en grense per klient-IP, hvis miljøet tillater det.

Die Konsequenz: MaxInFlight ist kein statischer Wert. Er hängt von Ihrer langsamsten, knappsten Ressource ab (DB, externe Systeme, lagring) 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. Hvor det er mulig, bygg i en streaming-orientert måte (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 blockiert man 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 (grov årsaksanalyse, uten å overse personvern)

Dette gir deg et signal om grensene er for strenge (for mange 429) eller for svake (høye WaitedMs, økende latenser). Og du ser om enkelte ruter dominerer. For Windows- og Linux-tjenester er dette avgjørende i hverdagen: Uten telemetri blir et ytelsesproblem raskt et gjettespill mellom nettverk, database, proxy og applikasjon.

Uvanlig, men ekstremt nyttig: „WaitedMs“ som tidlig varselindikator

Mange team ser kun på responstid og CPU. WaitedMs er ofte en bedre indikator, fordi den viser at forespørsler allerede før det egentlige arbeidet venter. Øker WaitedMs mens CPU forblir moderat, er den knappe ressursen ofte ikke CPU, men en pool (DB-tilkoblinger), en lås i forretningslogikken eller en ekstern downstream-tjeneste. Det sparer tid i årsaksanalyse fordi du kan søke mer målrettet mot „Pool/Lock/I/O“ i stedet for „kompilatoroptimalisering“.

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

Et Gate for alt er enkelt, men ikke alltid ideelt. Fornuftige varianter:

  • Gate per Route-Gruppe: „/reports“ strengt, „/api/orders“ moderat, „/health“ åpent. Slik forhindrer du at kostbare rapportforespørsler fortrenger kjerneprosesser.
  • Fast Lane für Admin/Monitoring: Eget Gate med liten parallellitet, slik at driftshandlinger fortsatt er mulig under last.
  • Budget-basierte Limits: Når responsstørrelser varierer sterkt, kan et byte-budsjett i tillegg hjelpe (f.eks. maksimalt X MB samtidig under generering). Dette er mer komplekst, men realistisk ved store nedlastinger.

Viktig: Prioritering blir fort politisk („min endepunkt er viktigere“). Teknisk stabilt forblir det når prioriteringer er koblet til prosesser (z.B. Auftragserfassung vor Reporting), ikke til roller eller avdelinger.

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

Et Concurrency-Gate er en pragmatisk byggestein for en High Performance REST Server i Delphi, fordi det gjør Overload kontrollerbart og holder systemene dine stabile ved Peak-Last. Det lønner seg spesielt dersom du har databasebundne Endpoints, en Reverse Proxy foran eller flere Clients (Legacy, Portale, Services) som genererer last i bølger.

Grensene er klare: Hvis det egentlige arbeidet per Request er for kostbart (ineffektive Queries, store JSON-Objekte, blokkerende Fremdsysteme), kamuflerer Gate bare symptomer. Da må dataaksess, Caching-Strategien, Timeouts og eventuelt asynkron Verarbeitung (Queue/Job-System) følges opp. Som en Sicherheitsgurt i drift er Gate ofte forskjellen mellom „kurz zäh“ og „komplett unbrauchbar“.

Hvis du vil innføre Overload-Verhalten i en eksisterende Delphi REST-API und REST-Server eller balansere grenser med database- og proxy-timeouts nøye: Diskuter prosjekt eller moderniseringsprosjekt med Net-Base.

I faglig sammenheng spiller også Thread-Pool Delphi og Http 429 Too Many Requests en viktig rolle når Integrationen, Datenflüsse og Weiterentwicklung godt må fungere sammen.

Diskuter prosjekt eller moderniseringsprosjekt med Net-Base.

Neste steg

Når et tema blir et reelt prosjekt, bør arkitektur, eksisterende systemer og drift tidlig vurderes samlet.

Vi bistår ikke bare med enkeltspørsmål, men også når kodesnutter, legacy-temaer eller portalideer skal utvikles til et robust virksomhetsprosjekt.

  • Eksisterende tilstand, målbildet og tekniske risikoer vurderes samlet.
  • REST, datatilgang, portaler og utrulling blir ikke utsatt som sene følger.
  • Dere ser tidlig hvilken vei som er økonomisk og driftsmessig levedyktig.

Del innlegg

Del dette innlegget direkte

LinkedIn, X, XING, Facebook, WhatsApp og e‑post er umiddelbart tilgjengelig. For Instagram forbereder vi lenke og kort tekst umiddelbart.

E-post

Instagram åpnes i en ny fane. Lenken og kortteksten kopieres først til utklippstavlen.