Net-Base Magasin

06.06.2026

Högpresterande REST-server i Delphi: förfrågningsgränser, trådpool och kontrollerat överbelastningsbeteende (källkodssnutt)

En high-performance REST-server i Delphi blir inte bara snabb genom 'snabbt JSON', utan genom kontrollerad parallellitet, hårda timeouter och ett rent överbelastningsbeteende. Det här inlägget visar en praktiskt tillämpbar Concurrency-Gate med semaphore, 429/503-svar...

06.06.2026

Från magasinets tema till projektpraxis

Passande tjänste- och tekniksidor för inlägget

Varför „High Performance“ hos REST i Delphi ofta misslyckas på grund av samtidighet

En High Performance REST Server Delphi är i praktiken sällan begränsad av ren CPU-tid per Request, utan av okontrollerad samtidighet: för många samtidiga Requests, för många samtidiga databas-queries eller blockerande I/O (fil, nätverk, databas). Resultatet upplevs då inte som ”lite långsammare”, utan som en kedjereaktion: fler trådar, längre köer, connection-pool-kollaps, ökande latenser, timeouts på klientsidan och i slutändan en server som visserligen fortfarande ”lever”, men inte längre levererar stabila svar.

Motmedlet är ingen enskild trick, utan ett medvetet överbelastningsbeteende: När servern når sina gränser måste den tidigt och deterministiskt neka (typiskt HTTP 429 eller 503), istället för att låta Requests köa i en oändlig väntetid. Just för detta är denna källkodssnutt avsedd: ett lättviktigt Concurrency-Gate (Semaphore) plus Timeouts, som kan integreras i befintliga REST-endpoints – oavsett om ni använder Indy, WebBroker, Horse eller ett eget HTTP-lager.

Arkitekturidé: Concurrency-Gate före den „kostsamma delen”

Grundidén är enkel: Innan den kostsamma delen (databasåtkomst, komplexa rapporter, stora JSON-svar) reserveras en token från en Semaphore. Finns ingen token ledig ges omedelbart ett kontrollerat svar. Viktigt är: Detta Gate måste pålitligt frigöras (try/finally), och det måste sitta i den kodväg som verkligen är dyr – inte bara i början av Request-handlern, när det ändå följer parser/router/autentisering efteråt.

På så sätt optimeras inte lasten bort, utan kanaliseras: Servern svarar på färre Requests samtidigt, men med stabilare latenser. I skräddarsydda företagsapplikationer är det oftast mer värdefullt än sporadiska rekordtider i syntetiska benchmarks.

Källkodssnutt: Request-Limiter med Timeout, 429/503 och Telemetri-Hooks

Följande Delphi-kod implementerar ett Concurrency-Gate som klassen TRestRequestGate. Det bygger på TSemaphore (från System.SyncObjs; en Semaphore är en räknare för begränsade samtidiga åtkomster). Gate-anropet returnerar antingen ett „Lease”-objekt (RAII-liknande: frigöring i destruktorn) eller bestämmer sig för ett omedelbart överbelastningssvar. Dessutom finns hooks för logging/monitoring, så att ni i drift kan se varför Requests avvisades.

Delphi
unit RESTRequestGate;

interface

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

type
  // Minimal kontext för loggning/spårning; kan t.ex. utökas med användare/route.
  TRESTGateContext = record
    RequestId: string;
    Route: string;
    RemoteIp: string;
  end;

  TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);

  // Hook för drifttelemetri (t.ex. fil, syslog, Prometheus-exporter, etc.)
  TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
                                         Decision: TRESTOverloadDecision;
                                         WaitedMs: Integer;
                                         InFlight: Integer);

  // Lease-objekt: frigör token i destruktorn.
  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 väntetid, omedelbart 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;

  // Minska först räknaren, frigör sedan 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åste vara > 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 vid TimeoutMs > 0: avsiktlig väntan men begränsad.
        Decision := odRejectedTimeout;
        Result := False;
        if Assigned(FOnEvent) then
          FOnEvent(Ctx, Decision, WaitedMs, InFlight);
      end;
  else
    begin
      // wrAbandoned/fel: avvisa konservativt.
      Decision := odRejectedBusy;
      Result := False;
      if Assigned(FOnEvent) then
        FOnEvent(Ctx, Decision, WaitedMs, InFlight);
    end;
  end;
end;

end.

Syfte: Stabilitet under belastning istället för „allt samtidigt“

Med MaxInFlight definierar du hur många requests som samtidigt får gå in i den „kostsamma delen“. Det är medvetet inte „antal CPU-kärnor“, utan en driftsparameter. För databasintensiva Endpoints är det ofta lämpligt att sätta MaxInFlight i relation till DB-Connection-Poolen (till exempel Pool = 20, MaxInFlight = 12 till 16), så att inte varje request blockerar en anslutning och därefter fler trådar dras in.

Randvillkor och fallgropar

  • Try/Finally är obligatoriskt: Leasen måste garanteras frigöras. Om du får Exceptions i endpointen blir annars Gate „läckande“ och servern förblir permanent „busy“.
  • Välj timeout med omsorg: TimeoutMs=0 är en hård gräns (avvisa omedelbart). En kort timeout (typiskt 50 till 150 ms) jämnar ut toppar utan att bygga upp riktiga köer.
  • Gate inte för tidigt: Autentisering (till exempel Bearer/JWT) eller routing kan vara lämpligt; semaforen bör placeras före den verkligt kostsamma sektionen. Omvänt: om autentisering är dyr (t.ex. mot ett externt identity-system) måste även den begränsas.
  • 429 vs 503: HTTP 429 („Too Many Requests“) passar när klienter avsiktligt ska göra retry. 503 („Service Unavailable“) passar när tjänsten temporärt generellt inte kan ta emot förfrågningar på ett meningsfullt sätt. I båda fallen är en Retry-After-header rekommenderbar.

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

Snippeten är avsiktligt ramverksneutral. Det behövs bara en plats där requests „passerar igenom“. Typiskt är ett globalt singleton eller ett Gate per routgrupp (till exempel „/reports“ mindre, „/health“ utan Gate). Exempel på införande som mönster:

  • Fyll kontexten (RequestId, Route, RemoteIp)
  • TryAcquire med kort timeout
  • Vid avslag skriv omedelbart en Response (429/503) och avsluta
  • Leasen lever i Scope tills efter den kostsamma delen

I Horse (Middleware) ligger Gate nära en routgrupp. I WebBroker kan du arbeta i respektive Action-Handler. För Indy beror det på om du har en tråd per request; Gateet verkar ändå så länge de kostsamma avsnitten är tydligt avgränsade.

High Performance REST Server Delphi: Overload-svar som inte „förgiftar“ klienter

Svar vid överbelastning är mer än statuskoder. Om klienter vid 429/503 aggressivt skickar igen omedelbart får du en retry-storm. I heterogena systemlandskap (mobilappar, C# Services, legacy-klienter) hjälper ett konsekvent beteende:

  • Retry-After: till exempel 1 till 3 sekunder, beroende på Endpoint. Det är en tydlig taktgivare.
  • Kort body: En liten JSON som {"error":"server_busy","requestId":"..."} räcker. Stora error-objekt kostar åter CPU och bandbredd.
  • Health-Endpoint utan nedreglering: Monitoring ska även vid last ge utsagor (eventuellt med „degraded“-flagga).

Om du kör en Reverse Proxy som nginx framför: stäm av Timeouts och Buffering där. En proxy kan avlasta (TLS-Termination, Keep-Alive), men också flytta belastning (till exempel buffra stora Request-Bodies). I drift är det väsentligt att begränsningarna är konsekventa: Proxy-Timeout > App-Timeout, annars ser klienterna „Gateway Timeout“, även om appen korrekt hade avvisat.

Trådning, DB-pooler och Keep-Alive: Var det brister i praktiken

Gate löser problemet med „för många samtidigt“, men det förhindrar inte automatiskt att en enskild request binder oproportionerligt mycket resurser. Tre typiska brytpunkter från Delphi-projekt uppstår precis i gränssnitten mellan trådning, databas och HTTP-anslutningar:

  • En request blockerar flera knappa resurser: Först en DB-anslutning, sedan ett externt HTTP-anrop, sedan åtkomst till filsystemet. Om allt detta sker i samma requesttråd multipliceras blockerings­tiden. Gate begränsar visserligen parallelliteten, men genomströmningen sjunker drastiskt. Här är det värt att lösgöra beroenden (t.ex. göra externa anrop asynkront, förberäkning via jobbkö).
  • BDE-Ablosung mit nativer Anbindung-Pooling und Transaktionen: BDE-Ablosung mit nativer Anbindung kan poola anslutningar, men en „lång“ transaktion (t.ex. för att JSON-generering eller affärsvalideringar ligger mellan StartTransaction och Commit) håller anslutningen onödigt länge. En ren praxis är att hålla transaktionen så snäv som möjligt kring de faktiska statements och, om det går ur domänsynpunkt, serialisera eller validera utanför transaktionen.
  • HTTP Keep-Alive som dold minnesätare: Keep-Alive minskar handskakningar men kan vid många inaktiva klienter leda till för många öppna sockets. Speciellt för Windows- och Linux-Services ser man då inte „CPU upp“, utan „Handles/FDs fulla“ eller RAM som växer pga. buffrar. Här hjälper tydliga idle-timeouts i servern och i reverse-proxyn samt en gräns per klient-IP om miljön tillåter.

Konsekvensen: MaxInFlight är inget statiskt värde. Det beror på din långsammaste, mest knappa resurs (DB, externa system, storage) och på hur väl en request „håller ihop“ dessa resurser.

Prestandaåtgärder utöver Gate: blanda inte JSON, DB och I/O

Gate stabiliserar, men ersätter inte en ren endpoint-ekonomi. Tre återkommande bromsar i Delphi REST-servrar dyker upp gång på gång:

  • JSON-byggande med onödiga mellansträngar: Ofta uppstår belastning genom många temporära Unicode-strängar. Bygg i möjligaste mån streaming-orienterat (writer/stream) istället för stora mellanobjekt, särskilt vid lista-endpoints.
  • Databasåtkomst „per item“: N+1-queries och per-rad-uppslag är klassikern. Bättre är riktade joins, batch-queries och serversidig aggregering. Vid mycket stora resultat lönar sig också paginering med stabil sortering (så att sidorna inte „hoppar“).
  • Blockerande I/O i requesttråden: Filåtkomst eller externa HTTP-anrop bör antingen strikt begränsas eller flyttas till en asynkron pipeline. Annars blockerar ni dyra trådar för „väntan“.

För växande digitala företagslösningar är detta ofta knäckfrågan: En endpoint lades till „snabbt“ och fungerar tills verklig last och datavolymer kommer. Då visar det sig om arkitekturgränserna är tydligt dragna (dataåtkomstlager, caching, bulk-strategier, tydliga timeouts).

Debugging och drift: Vad ni bör mäta

Hooken OnEvent är medvetet enkel. I praktiken bör ni åtminstone samla in följande värden:

  • InFlight (aktuell parallellitet vid Gate)
  • WaitedMs (hur mycket köbildning ni tillåter)
  • Decision (accepted/busy/timeout)
  • Route/RemoteIp (grov orsaksanalys, utan att ignorera dataskyddet)

Det ger er en signal om huruvida gränserna är för stränga (för många 429) eller för lösa (höga WaitedMs, stigande latenser). Och ni ser om enskilda rutter dominerar. För Windows- och Linux-tjänster är detta avgörande i vardagen: Utan telemetri blir ett prestandaproblem snabbt ett gissningsspel mellan nätverk, databas, proxy och applikation.

Ovanligt, men extremt användbart: „WaitedMs“ som tidig varningsindikator

Många team tittar bara på responstid och CPU. WaitedMs är ofta den bättre indikatorn, eftersom den visar att requests redan väntar före det verkliga arbetet. Om WaitedMs stiger medan CPU förblir måttlig är den knappaste resursen ofta inte CPU utan en pool (DB-anslutningar), ett lås i affärslogiken eller en extern downstream-tjänst. Det sparar tid vid orsaksanalys eftersom ni kan söka mer riktat mot „Pool/Lock/I/O“ istället för „kompilatoroptimering“.

Varianter: Gate per rutt, prioriteringar och „Fast Lane“

En gate för allt är enkelt, men inte alltid idealiskt. Meningsfulla varianter:

  • Gate per ruttgrupp: „/reports“ strikt, „/api/orders“ måttlig, „/health“ öppen. Så förhindrar ni att kostsamma rapportförfrågningar tränger undan kärnprocesser.
  • Fast Lane för admin/övervakning: Separat gate med låg parallellitet, så att driftåtgärder också är möjliga under belastning.
  • Budgetbaserade gränser: När responsstorlekar varierar kraftigt kan ett byte-budget dessutom hjälpa (t.ex. maximalt X MB samtidigt under generering). Det är mer komplext, men realistiskt vid stora nedladdningar.

Viktigt: Prioritering blir snabbt politisk („min Endpoint är viktigare“). Tekniskt stabilt förblir det om prioriteringar kopplas till processer (t.ex. orderinmatning före rapportering), inte till roller eller avdelningar.

Slutsats: Är ett Concurrency-Gate värt det – och när brister angreppssättet?

Ett Concurrency-Gate är en pragmatisk byggsten för en High Performance REST-server i Delphi, eftersom det gör överbelastning hanterbar och håller era system stabila vid peak-last. Det lönar sig särskilt om ni har databaskopplade endpoints, om en reverse proxy står framför eller om flera klienter (Legacy, portaler, tjänster) skapar belastning i vågor.

Gränserna är tydliga: Om det verkliga arbetet per request är för dyrt (ineffektiva queries, stora JSON-objekt, blockerande externa system) maskerar Gate bara symtomen. Då måste dataåtkomst, cachningsstrategier, timeouter och eventuellt asynkron bearbetning (kö-/jobbsystem) åtgärdas. Som säkerhetsbälte i drift är Gate ofta skillnaden mellan „kort segt“ och „helt obrukbart“.

Om ni vill införa overload-beteende i en befintlig Delphi REST-API och REST-Server eller balansera gränser i samklang med databas- och proxy-timeouter: diskutera projekt eller moderniseringsinitiativ med Net-Base.

I det tekniska sammanhanget spelar även thread-pool Delphi och HTTP 429 Too Many Requests en viktig roll när integrationer, dataflöden och vidareutveckling måste samspela väl.

Diskutera projekt eller moderniseringsinitiativ med Net-Base.

Nästa steg

När ett ämne blir ett verkligt projekt bör arkitektur, befintliga system och drift behandlas gemensamt redan i ett tidigt skede.

Vi stöder inte bara vid enstaka frågor, utan även när kodsfragment, legacy-frågor eller portalidéer ska utvecklas till ett robust företagsprojekt.

  • Nuläge, målbild och tekniska risker bedöms tillsammans.
  • REST, dataåtkomst, portaler och utrullning skjuts inte upp som sena följder.
  • Ni ser tidigt vilken väg som är ekonomiskt och driftsmässigt bärkraftig.

Dela inlägg

Dela det här inlägget direkt

LinkedIn, X, XING, Facebook, WhatsApp och e‑post är omedelbart tillgängliga. För Instagram förbereder vi länken och en kort text direkt.

E-post

Instagram öppnas i en ny flik. Länken och korttexten kopieras till urklipp först.