Net-Base Revista

06.06.2026

Servidor de alto rendimiento REST en Delphi: límites de solicitudes, pool de hilos y comportamiento controlado ante sobrecarga (fragmento de código fuente)

Un servidor REST de alto rendimiento en Delphi no es rápido únicamente por un «JSON rápido», sino por concurrencia controlada, timeouts estrictos y un comportamiento de sobrecarga limpio. Este artículo muestra un Concurrency-Gate práctico con Semaphore y respuestas 429/503.

06.06.2026

Del tema de la revista a la práctica del proyecto

Páginas de servicios y técnicas relacionadas

Por qué «High Performance» en REST en Delphi suele fracasar por la concurrencia

Un servidor REST de High Performance en Delphi rara vez está limitado en la práctica por el tiempo de CPU por petición, y sí por una concurrencia incontrolada: demasiadas peticiones simultáneas, demasiadas consultas a la base de datos en paralelo o E/S bloqueante (archivo, red, base de datos). El resultado no se percibe como «un poco más lento», sino como una reacción en cadena: más hilos, más colas de espera, colapso del pool de conexiones, latencias crecientes, timeouts en el lado del cliente y, al final, un servidor que aún «vive», pero que ya no entrega respuestas estables.

El remedio no es un truco aislado, sino un comportamiento de sobrecarga consciente: cuando el servidor alcanza sus límites, debe rechazar temprana y de forma determinista (típicamente HTTP 429 u 503), en lugar de dejar que las peticiones se acumulen en una cola infinita. Para eso sirve este fragmento de código fuente: una puerta de concurrencia ligera (Semaphore) con timeouts, que se puede integrar en endpoints REST existentes, independientemente de si utiliza Indy, WebBroker, Horse o su propia capa HTTP.

Idea de arquitectura: Concurrency-Gate antes de la «parte costosa»

La idea básica es sencilla: antes de la parte costosa (acceso a base de datos, informes complejos, grandes respuestas JSON) se reserva un token de una Semaphore. Si no hay token disponible, se devuelve inmediatamente una respuesta controlada. Es importante: esta puerta debe liberarse de manera fiable (try/finally), y debe estar en la ruta de código que realmente es costosa —no solo al principio del manejador de peticiones, cuando después aún vienen parser/enrutador/autenticación.

Con ello, la carga no se «elimina», sino que se canaliza: el servidor atiende menos peticiones simultáneamente, pero con latencias más estables. En aplicaciones empresariales a medida esto suele ser más valioso que tiempos puntuales óptimos en benchmarks sintéticos.

Fragmento de código fuente: limitador de peticiones con timeout, 429/503 y ganchos de telemetría

El siguiente código de Delphi implementa un Concurrency-Gate como clase TRestRequestGate. Se basa en TSemaphore (desde System.SyncObjs; una Semaphore es un contador para accesos concurrentes limitados). La invocación del Gate devuelve o bien un «Lease»-objeto (similar a RAII: liberación en el destructor) o bien decide por una respuesta de sobrecarga inmediata. Además hay hooks para logging/monitoring, de modo que en producción pueda ver por qué se rechazaron peticiones.

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 muss > 0 sein');

  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.

Propósito: estabilidad bajo carga en lugar de ‚todo a la vez‘

Con MaxInFlight define cuántas solicitudes pueden entrar simultáneamente en la „parte costosa“. Esto conscientemente no es el „número de núcleos de CPU“, sino una magnitud operativa. En endpoints con alta carga de base de datos suele tener sentido ajustar MaxInFlight en relación con el pool de conexiones de la base de datos (por ejemplo Pool = 20, MaxInFlight = 12 a 16), para evitar que cada solicitud bloquee una conexión y a continuación tiren de más hilos.

Condiciones y puntos críticos

  • Try/Finally es obligatorio: Hay que garantizar la liberación del Lease. Si tiene excepciones en el endpoint, el gate se volverá „permeable“ y el servidor permanecerá de forma permanente en „busy“.
  • Elegir un Timeout razonable: TimeoutMs=0 es un límite rígido (rechazo inmediato). Un timeout corto (típicamente 50 a 150 ms) suaviza picos sin crear colas reales.
  • No colocar el gate demasiado pronto: La autenticación (por ejemplo Bearer/JWT) o el enrutamiento pueden ser económicos; el semáforo debería actuar antes de la sección realmente costosa. A la inversa: si la autenticación es costosa (p. ej. contra un sistema de identidad externo), también debe limitarse.
  • 429 vs 503: HTTP 429 („Too Many Requests“) encaja bien cuando se espera que los clientes reintenten de forma dirigida. 503 („Service Unavailable“) encaja cuando el servicio, de forma temporal, no está en condiciones de aceptar solicitudes de manera útil. En ambos casos es recomendable un encabezado Retry-After.

Integración en REST-Handler: Indy/WebBroker/Horse pragmático

El snippet es intencionadamente neutral respecto al framework. Solo necesita un punto donde las solicitudes „pasen“. Lo habitual es un singleton global o un gate por grupo de rutas (por ejemplo „/reports“ más pequeño, „/health“ sin gate). A modo de patrón, la integración ejemplar:

  • Rellenar el contexto (RequestId, Route, RemoteIp)
  • TryAcquire con timeout corto
  • En caso de rechazo escribir la respuesta inmediatamente (429/503) y terminar
  • El Lease vive en el scope hasta después de la parte costosa

En Horse (middleware) el gate queda cercano a un grupo de rutas. En WebBroker puede trabajar en el action-handler correspondiente. En Indy depende de si tiene un hilo por request; el gate sigue funcionando siempre que las secciones costosas estén claramente acotadas.

Servidores REST de alto rendimiento Delphi: respuestas por sobrecarga que no „envenenan“ a los clientes

Las respuestas por sobrecarga son más que códigos de estado. Si los clientes, ante 429/503, vuelven a enviar de forma agresiva e inmediata, se genera una tormenta de reintentos. En paisajes de sistemas heterogéneos (apps móviles, C# Servicios, clientes legacy) ayuda un comportamiento consistente:

  • Retry-After: por ejemplo 1 a 3 segundos, según el endpoint. Es un marcador de ritmo claro.
  • Cuerpo corto: Un pequeño JSON como {"error":"server_busy","requestId":"..."} es suficiente. Objetos de error grandes vuelven a costar CPU y ancho de banda.
  • Health-Endpoint sin limitar: El monitoring debe seguir proporcionando información incluso bajo carga (posiblemente con un flag „degraded“).

Si opera un reverse proxy como nginx delante: ajuste allí los timeouts y el buffering. Un proxy puede aliviar (TLS-termination, Keep-Alive), pero también desplazar la carga (por ejemplo almacenando en búfer bodies de request grandes). En producción importa que los límites sean consistentes: Proxy-Timeout > App-Timeout, de lo contrario los clientes ven un „Gateway Timeout“ aunque la app hubiera rechazado correctamente.

Threading, DB-Pools y Keep-Alive: Dónde se desestabiliza en la práctica

El Gate resuelve el problema de «demasiados al mismo tiempo», pero no evita automáticamente que una sola petición ate recursos en exceso. Tres puntos críticos típicos de proyectos Delphi surgen precisamente en las interfaces entre threading, base de datos y conexiones HTTP:

  • Una petición bloquea varios recursos escasos: Primero una conexión a la DB, luego una llamada HTTP externa, luego un acceso a archivos. Si todo esto sucede en el mismo hilo de la petición, el tiempo de bloqueo se multiplica. El Gate limita la concurrencia, pero el rendimiento de throughput cae drásticamente. Conviene desacoplar las dependencias (p. ej., llamadas externas asíncronas, preprocesado mediante cola de trabajos).
  • BDE-Ablosung mit nativer Anbindung-Pooling und Transaktionen: BDE-Ablosung mit nativer Anbindung puede gestionar un pool de conexiones, pero una transacción «larga» (p. ej. porque la creación de JSON o las comprobaciones de negocio se sitúan entre StartTransaction y Commit) mantiene la conexión innecesariamente. Una práctica correcta es delimitar la transacción lo más estrechamente posible alrededor de las sentencias reales y serializar o validar fuera de la transacción cuando lo permita la lógica del negocio.
  • HTTP Keep-Alive como consumidor oculto de memoria: Keep-Alive reduce los handshakes, pero con muchos clientes inactivos puede generar demasiados sockets abiertos. Sobre todo en Windows- und Linux-Services no se ve «CPU alta», sino «Handles/FDs llenos» o uso de RAM por buffers. Aquí ayudan timeouts de inactividad claros en el servidor y en el proxy inverso, así como un límite por IP cliente cuando el entorno lo permite.

La consecuencia: MaxInFlight no es un valor estático. Depende de su recurso más lento y más limitado (DB, sistemas externos, Storage) y de qué tan bien una petición «retiene» esos recursos.

Palancas de rendimiento además del Gate: no mezclar JSON, DB e I/O

El Gate estabiliza, pero no reemplaza una economía de endpoints bien definida. Tres frenos en servidores Delphi REST aparecen repetidamente:

  • Construcción de JSON con cadenas intermedias innecesarias: A menudo la carga proviene de muchas cadenas Unicode temporales. Cuando sea posible, construir orientado a streaming (Writer/Stream) en lugar de enormes objetos intermedios, especialmente en endpoints de listas.
  • Acceso a la base de datos «por item»: Las N+1-Queries y las búsquedas por fila son el clásico. Mejor: joins seleccionados, consultas por lotes (batch-queries), agregación en el servidor. En resultados muy grandes conviene además paginación con ordenación estable (para que las páginas no «salten»).
  • I/O bloqueante en el hilo de la petición: Accesos a archivos o llamadas HTTP externas deberían limitarse estrictamente o trasladarse a una pipeline asíncrona. Si no, se están bloqueando hilos costosos en «espera».

Para soluciones empresariales digitales ya crecidas, ese suele ser el punto crítico: un endpoint se añadió «rápidamente» y funciona hasta que llega la carga real y los volúmenes de datos. Entonces se ve si los límites arquitectónicos se trazaron de forma limpia (capa de acceso a datos, caching, estrategias de procesamiento por lotes, timeouts claros).

Debugging und Betrieb: Was Sie messen sollten

El hook OnEvent es deliberadamente simple. En la práctica debería registrar al menos los siguientes valores:

  • InFlight (concurrencia actual en el Gate)
  • WaitedMs (cuánto encolamiento permite)
  • Decision (accepted/busy/timeout)
  • Route/RemoteIp (análisis preliminar de causas, sin ignorar la protección de datos)

Con ello obtiene una señal de si los límites son demasiado estrictos (demasiados 429) o demasiado laxos (alto WaitedMs, latencias en aumento). Y ve si rutas individuales dominan. Para Windows- y Linux-Services esto es decisivo en el día a día: sin telemetría, un problema de rendimiento pronto se convierte en un juego de adivinanzas entre red, base de datos, proxy y aplicación.

Inusual, pero extremadamente útil: «WaitedMs» como indicador de alerta temprana

Muchos equipos solo observan el tiempo de respuesta y la CPU. WaitedMs suele ser el indicador más útil, porque muestra que las solicitudes ya están esperando antes del trabajo real. Si WaitedMs sube mientras la CPU se mantiene moderada, el recurso escaso con frecuencia no es la CPU, sino un pool (conexiones DB), un lock en la lógica de negocio o un servicio downstream externo. Eso ahorra tiempo en el análisis de causas, porque permite buscar de forma más dirigida hacia «Pool/Lock/I/O» en lugar de «optimización del compilador».

Variantes: Gate por ruta, prioridades y «Fast Lane»

Un gate para todo es sencillo, pero no siempre ideal. Variantes sensatas:

  • Gate por grupo de rutas: „/reports“ estricto, „/api/orders“ moderado, „/health“ abierto. Así evita que solicitudes de informes costosas desplacen procesos núcleo.
  • Fast Lane para administración/monitorización: Gate separado con baja paralelidad, para que las tareas operativas sean posibles incluso bajo carga.
  • Límites basados en presupuesto: Si los tamaños de respuesta varían mucho, puede ayudar adicionalmente un presupuesto de bytes (p. ej., máximo X MB simultáneamente en la generación). Es más complejo, pero realista para descargas grandes.

Importante: la priorización se vuelve rápidamente política („mi endpoint es más importante“). Técnicamente se mantiene estable cuando las prioridades están vinculadas a procesos (p. ej., captura de pedidos antes de reporting), no a roles o departamentos.

Conclusión: ¿Merece la pena el Gate — y en qué punto falla el enfoque?

Un Concurrency-Gate es un componente pragmático para un servidor High Performance REST en Delphi, porque hace la sobrecarga controlable y mantiene sus sistemas estables ante picos de carga. Merece la pena especialmente si tiene endpoints ligados a base de datos, si hay un reverse proxy delante o si varios clientes (Legacy, portales, servicios) generan carga en oleadas.

Los límites están claros: si el trabajo real por solicitud es demasiado costoso (consultas ineficientes, objetos JSON grandes, sistemas externos que bloquean), el Gate solo enmascara síntomas. Entonces hay que mejorar acceso a datos, estrategias de caché, timeouts y, si procede, procesamiento asíncrono (Queue/Job-System). Como cinturón de seguridad en explotación, el Gate a menudo marca la diferencia entre „algo lento“ y „completamente inutilizable“.

Si desea incorporar comportamiento ante sobrecarga en una Delphi REST-API y REST-Server, o ajustar cuidadosamente límites con timeouts de base de datos y proxy: consulte el proyecto o la iniciativa de modernización con Net-Base.

En el ámbito técnico también juegan un papel importante el Thread-Pool Delphi y el HTTP 429 Too Many Requests, cuando integraciones, flujos de datos y evolución deben funcionar de manera coherente.

Discutir un proyecto o iniciativa de modernización con Net-Base.

Siguiente paso

Cuando el tema se convierte en un proyecto real, la arquitectura, los sistemas existentes y la operación deben considerarse desde el principio.

No solo apoyamos en consultas puntuales, sino también cuando, a partir de fragmentos de código fuente, temas heredados o ideas de portales, debe consolidarse un proyecto empresarial robusto.

  • La situación actual, el estado objetivo y los riesgos técnicos se evalúan conjuntamente.
  • REST, el acceso a datos, los portales y el rollout no se posponen como consecuencias tardías.
  • Detecta con antelación qué enfoque es viable desde el punto de vista económico y operativo.

Compartir entrada

Compartir esta publicación directamente

LinkedIn, X, XING, Facebook, WhatsApp y correo electrónico están disponibles de inmediato. Para Instagram preparamos el enlace y un texto breve de inmediato.

Correo electrónico

Instagram se abre en una nueva pestaña. El enlace y el texto breve se copian previamente en el portapapeles.