Net-Base Rivista

06.06.2026

Server ad alte prestazioni REST in Delphi: limiti di richieste, pool di thread e comportamento controllato in caso di sovraccarico (frammento di codice sorgente)

Un server ad alte prestazioni REST in Delphi non è veloce soltanto grazie al «JSON veloce», ma per via del parallelismo controllato, di timeout rigorosi e di una gestione pulita del sovraccarico. Questo articolo mostra un Concurrency-Gate pratico con Semaphore, risposte 429/503...

06.06.2026

Dal tema della rivista alla pratica di progetto

Pagine di servizi e tecniche correlate all'articolo

Perché „High Performance“ in REST in Delphi spesso fallisce a causa della concorrenza

Un server REST Delphi ad alte prestazioni è nella pratica raramente limitato dal solo tempo CPU per richiesta, ma dalla concorrenza incontrollata: troppe richieste simultanee, troppe query al database in parallelo o I/O bloccanti (file, rete, database). Il risultato non appare come „un po‘ più lento“, ma come una reazione a catena: più thread, più code d’attesa, collasso del connection pool, aumento delle latenze, timeout lato client e infine un server che pur essendo ancora „in funzione“ non fornisce più risposte stabili.

Il rimedio non è un singolo trucco, ma un comportamento consapevole di sovraccarico: quando il server raggiunge i limiti deve rifiutare precocemente e in modo deterministico (tipicamente HTTP 429 o 503), invece di lasciare le richieste in una coda infinita. Proprio per questo è pensato questo frammento di sorgente: un leggero gate di concorrenza (Semaphore) con timeout, che può essere integrato negli endpoint REST esistenti — indipendentemente dal fatto che utilizziate Indy, WebBroker, Horse o uno strato HTTP proprietario.

Idea architetturale: gate di concorrenza prima della „parte costosa“

L’idea di base è semplice: prima della parte costosa (accesso al database, report complessi, grandi risposte JSON) si riserva un token da una semaphore. Se non è disponibile alcun token, viene restituita immediatamente una risposta controllata. È importante: questo gate deve essere rilasciato in modo affidabile (try/finally), e deve trovarsi nel flusso di codice che è veramente costoso — non solo all’inizio dell’handler della richiesta, se dopo sono ancora presenti parser/router/autenticazione.

In questo modo il carico non viene „eliminato“, ma canalizzato: il server risponde a meno richieste contemporaneamente, ma con latenze più stabili. Nelle applicazioni aziendali personalizzate questo è di solito più prezioso rispetto a occasionali tempi record in benchmark sintetici.

Frammento di sorgente: limitatore di richieste con timeout, 429/503 e hook di telemetria

Il seguente codice Delphi implementa un gate di concorrenza come classe TRestRequestGate. Si basa su TSemaphore (da System.SyncObjs; una semaphore è un contatore per accessi concorrenti limitati). La chiamata al gate restituisce o un oggetto „Lease“ (simile a RAII: rilascio nel Distruttore) oppure decide per una immediata risposta di sovraccarico. In aggiunta sono presenti hook per logging/monitoring, in modo che durante il funzionamento possiate vedere perché le richieste sono state respinte.

Delphi
unit RESTRequestGate;

interface

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

type
  // Contesto minimo per logging/tracing; può ad es. essere esteso con User/Route.
  TRESTGateContext = record
    RequestId: string;
    Route: string;
    RemoteIp: string;
  end;

  TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);

  // Hook per telemetria operativa (p.es. su file, Syslog, Prometheus-Exporter, ecc.)
  TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
                                         Decision: TRESTOverloadDecision;
                                         WaitedMs: Integer;
                                         InFlight: Integer);

  // Lease-Objekt: rilascio del token nel distruttore.
  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: nessun tempo d'attesa, subito 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;

  // Prima decrementare il contatore, poi rilasciare il semaforo.
  TInterlocked.Decrement(FInFlightCounter^);
  FSemaphore.Release;
end;

{ TRESTRequestGate }

constructor TRESTRequestGate.Create(AMaxInFlight: Integer);
begin
  inherited Create;
  if AMaxInFlight <= 0 then
    raise EArgumentException.Create('AMaxInFlight deve > 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 per TimeoutMs > 0: attesa mirata, ma limitata.
        Decision := odRejectedTimeout;
        Result := False;
        if Assigned(FOnEvent) then
          FOnEvent(Ctx, Decision, WaitedMs, InFlight);
      end;
  else
    begin
      // wrAbandoned/casi di errore: rifiutare in modo conservativo
      Decision := odRejectedBusy;
      Result := False;
      if Assigned(FOnEvent) then
        FOnEvent(Ctx, Decision, WaitedMs, InFlight);
    end;
  end;
end;

end.

Scopo: stabilità sotto carico anziché «tutto contemporaneamente»

Con MaxInFlight definite quanti request possono entrare contemporaneamente nella «parte costosa». Questo non corrisponde intenzionalmente al «numero di core CPU», ma è una grandezza operativa. Per endpoint con forte carico sul database spesso ha senso impostare MaxInFlight in relazione al pool di connessioni al DB (per esempio Pool = 20, MaxInFlight = 12 bis 16), in modo che non ogni request blocchi una connessione e poi altri thread restino in attesa.

Vincoli e insidie

  • Try/Finally è obbligatorio: il lease deve essere rilasciato in modo garantito. Se avete eccezioni nell’endpoint, altrimenti il gate diventa «perdente» e il server rimane permanentemente in stato «busy».
  • Scegliere un timeout sensato: TimeoutMs=0 è un limite rigido (rifiuto immediato). Un timeout breve (tipicamente 50 bis 150 ms) smussa i picchi senza costruire vere code di attesa.
  • Non porre il gate troppo presto: l’autenticazione (per esempio Bearer/JWT) o il routing possono essere eseguiti in anticipo; la semafora dovrebbe entrare in funzione prima della sezione realmente costosa. Al contrario: se l’autenticazione diventa costosa (p.es. verso un sistema di identity esterno), anche quella va limitata.
  • 429 vs 503: HTTP 429 («Too Many Requests») è adeguato quando i client devono riprovare in modo mirato. 503 («Service Unavailable») è appropriato quando il servizio temporaneamente non è in grado di accettare richieste in modo sensato. In entrambi i casi è consigliabile un header Retry-After.

Integrazione nei REST-Handler: Indy/WebBroker/Horse pragmatico

Lo snippet è volutamente framework-neutral. Serve solo un punto in cui le request «passano». Tipico è un singleton globale o un gate per gruppo di route (per esempio «/reports» più restrittivo, «/health» senza gate). Di seguito un esempio di integrazione come modello:

  • Popolare il contesto (RequestId, Route, RemoteIp)
  • TryAcquire con timeout breve
  • In caso di rifiuto scrivere subito la response (429/503) e terminare
  • Il lease vive nello scope fino a dopo la parte costosa

In Horse (middleware) il gate è vicino a un gruppo di route. In WebBroker potete operare nel rispettivo action-handler. Con Indy dipende dal fatto se avete un thread per request; il gate funziona comunque, purché le sezioni costose siano delimitate con chiarezza.

Server REST ad alta performance Delphi: risposte di sovraccarico che non «avvelenano» i client

Le risposte di sovraccarico sono più che codici di stato. Se i client, di fronte a 429/503, rispediscono aggressivamente subito la richiesta, si genera uno storm di retry. In paesaggi di sistemi eterogenei (Mobile Apps, C# Services, legacy client) aiuta un comportamento coerente:

  • Retry-After: per esempio 1 bis 3 secondi, a seconda dell’endpoint. È un chiaro indicatore temporale.
  • Body ridotto: un piccolo JSON come {"error":"server_busy","requestId":"..."} è sufficiente. Oggetti di errore voluminosi consumano di nuovo CPU e larghezza di banda.
  • Health-Endpoint non sottoposto a throttling: il monitoring deve poter fornire indicazioni anche sotto carico (eventualmente con flag «degraded»).

Se avete un reverse proxy come nginx davanti: coordinate lì timeouts e buffering. Un proxy può alleggerire il carico (TLS-Termination, Keep-Alive), ma anche spostare il carico (per esempio bufferizzare grandi request-body). In esercizio conta che i limiti siano coerenti: Proxy-Timeout > App-Timeout, altrimenti i client vedranno «Gateway Timeout», anche se l’app avrebbe respinto correttamente.

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

Il Gate risolve il problema dei „troppi contemporaneamente“, ma non impedisce automaticamente che una singola richiesta leghi risorse in modo eccessivo. Tre punti tipici di rottura emersi nei progetti Delphi si manifestano proprio alle interfacce tra threading, database e connessioni HTTP:

  • Una richiesta blocca più risorse scarse: prima una connessione DB, poi una chiamata HTTP esterna, poi un accesso a file. Se tutto questo avviene nello stesso thread di richiesta, i tempi di blocco si moltiplicano. Il Gate limita la parallelità, ma il throughput cala drasticamente. Conviene disaccoppiare le dipendenze (es. chiamate esterne asincrone, precalcolo tramite Job-Queue).
  • BDE-Ablosung mit nativer Anbindung-Pooling und Transaktionen: BDE-Ablosung mit nativer Anbindung può poolare le connessioni, ma una transazione „lunga“ (es. perché la creazione di JSON o controlli di business avvengono tra StartTransaction e Commit) mantiene la connessione inutilmente. Una buona pratica è circoscrivere la transazione il più possibile attorno alle istruzioni effettive e serializzare o validare fuori dalla transazione quando la logica di dominio lo consente.
  • HTTP Keep-Alive come consumo nascosto di risorse: Keep-Alive riduce gli handshake, ma con molti client inattivi può generare troppi socket aperti. Soprattutto nei Windows- und Linux-Services non si vede „CPU alta“, ma „Handles/FDs pieni“ o RAM dovuta ai buffer. Qui aiutano timeout di idle chiari sul server e sul reverse proxy e un limite per client-IP, se l’ambiente lo consente.

La conseguenza: MaxInFlight non è un valore statico. Dipende dalla sua risorsa più lenta o più scarsa (DB, sistemi esterni, storage) e da quanto una richiesta tiene „insieme“ queste risorse.

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

Il Gate stabilizza, ma non sostituisce una corretta economia degli endpoint. Tre freni ricorrenti nei server Delphi REST emergono spesso:

  • Costruzione JSON con stringhe intermedie non necessarie: spesso il carico nasce da molte stringhe Unicode temporanee. Dove possibile, costruire orientati allo streaming (Writer/Stream) invece di creare grossi oggetti intermedi, specialmente per endpoint che restituiscono liste.
  • Accesso al database „per item“: N+1-Queries e lookup per riga sono il classico problema. Meglio: join mirati, query in batch, aggregazione lato server. Per risultati molto grandi conviene anche la paginazione con ordinamento stabile (così le pagine non „saltano“).
  • I/O bloccante nel thread di richiesta: accessi a file o chiamate HTTP esterne dovrebbero essere strettamente limitati o spostati in una pipeline asincrona. Altrimenti si occupano thread costosi in attesa.

Per soluzioni digitali aziendali cresciute nel tempo questo è spesso il punto critico: un endpoint è stato „aggiunto in fretta“ e funziona finché non arrivano carichi reali e volumi di dati. Allora si vede se i confini architetturali sono stati tracciati correttamente (layer di accesso ai dati, caching, strategie bulk, timeout chiari).

Debugging und Betrieb: Was Sie messen sollten

Il hook OnEvent è volutamente semplice. In pratica dovreste almeno raccogliere i seguenti valori:

  • InFlight (parallelismo corrente al Gate)
  • WaitedMs (quanto „queueing“ state consentendo)
  • Decision (accepted/busy/timeout)
  • Route/RemoteIp (analisi preliminare delle cause, senza ignorare la protezione dei dati)

Questo fornisce un segnale se i limiti sono troppo severi (troppi 429) o troppo permissivi (alte WaitedMs, latenze in aumento). E mostra se singole route predominano. Per Windows- und Linux-Services questo è cruciale nella pratica: senza telemetria un problema di performance diventa rapidamente un gioco d’ipotesi tra rete, database, proxy e applicazione.

Inusuale, ma estremamente utile: „WaitedMs“ come indicatore di allerta precoce

Molti team guardano solo la Response-Time e la CPU. WaitedMs è spesso il miglior indicatore, perché mostra che le richieste stanno già aspettando prima del lavoro vero e proprio. Se WaitedMs aumenta mentre la CPU resta moderata, la risorsa scarsa spesso non è la CPU, ma un pool (connessioni DB), un lock nella logica di business o un servizio downstream esterno. Questo fa risparmiare tempo nell’analisi delle cause, perché potete cercare in modo mirato verso „Pool/Lock/I/O“ invece che verso „Ottimizzazione del compilatore“.

Varianti: Gate per route, priorità e „Fast Lane“

Un gate per tutto è semplice, ma non sempre ideale. Varianti sensate:

  • Gate per gruppo di route: „/reports“ severo, „/api/orders“ moderato, „/health“ aperto. In questo modo evitate che richieste di report costose soppiantino i processi core.
  • Fast Lane per Admin/Monitoring: gate separato con basso parallelismo, in modo che le operazioni di gestione siano possibili anche sotto carico.
  • Limiti basati su budget: se le dimensioni delle response variano molto, un budget in byte può aiutare (es. massimo X MB generati contemporaneamente). È più complesso, ma realistico per download di grandi dimensioni.

Importante: la prioritizzazione diventa rapidamente politica („il mio endpoint è più importante“). Rimane tecnicamente stabile se le priorità sono collegate ai processi (es. registrazione ordini prima del reporting), non a ruoli o reparti.

Conclusione: vale la pena il gate — e dove può fallire l’approccio?

Un Concurrency-Gate è un elemento pragmatico per un server High Performance REST in Delphi, poiché rende l’overload gestibile e mantiene stabili i vostri sistemi sotto picchi di carico. È particolarmente utile se avete endpoint legati al database, se davanti c’è un reverse proxy o se più client (Legacy, Portale, Services) generano carico a ondate.

I limiti sono chiari: se il lavoro effettivo per request è troppo oneroso (query inefficaci, grandi oggetti JSON, sistemi esterni bloccanti), il gate maschera solo i sintomi. In quel caso vanno rivisti accesso ai dati, strategie di caching, timeout e, se necessario, l’elaborazione asincrona (Queue/Job-System). Come cintura di sicurezza in esercizio, comunque, il gate è spesso la differenza tra „leggermente lento“ e „completamente inutilizzabile“.

Se desiderate integrare il comportamento di overload in una Delphi REST-API e REST-Server esistente, oppure bilanciare correttamente i limiti con i timeout di database e proxy: discutete il progetto o l’iniziativa di modernizzazione con Net-Base.

Nel contesto tecnico anche il Thread-Pool Delphi e l’HTTP 429 Too Many Requests giocano un ruolo importante, quando integrazioni, flussi di dati e sviluppo devono cooperare senza attriti.

Discutere un progetto o un’iniziativa di modernizzazione con Net-Base.

Passo successivo

Quando un tema diventa un progetto reale, architettura, sistemi esistenti e gestione operativa dovrebbero essere considerati insieme fin dall'inizio.

Non forniamo solo supporto per questioni isolate, ma anche quando da frammenti di codice sorgente, tematiche legacy o idee di portale deve nascere un progetto aziendale solido.

  • Stato attuale, stato obiettivo e rischi tecnici vengono valutati insieme.
  • REST, l'accesso ai dati, i portali e il rollout non vengono rimandati a fasi successive.
  • Vede in anticipo quale percorso è economicamente ed operativamente sostenibile.

Condividi il post

Condividi direttamente questo articolo

LinkedIn, X, XING, Facebook, WhatsApp e e-mail sono immediatamente disponibili. Per Instagram prepariamo direttamente il link e un breve testo.

E-mail

Instagram si apre in una nuova scheda. Il link e il breve testo vengono copiati prima negli appunti.