Providers & Application types

July 12, 2026 · View on GitHub

Read this in English or Português (BR).

Horse separates two architectural choices that are easy to confuse:

  1. Provider — the HTTP transport that owns the socket and parses requests. Indy is the default; CrossSocket and mORMot2 are optional async alternatives; future providers (nghttp2, …) follow the same pattern.
  2. Application type — how the Delphi/FPC binary is packaged and started: a console executable, a Windows service, a VCL desktop app, an Apache module, an IIS extension, and so on.

These two axes are conceptually orthogonal. In the current Horse build the two choices are encoded in the same set of mutually-exclusive Conditional Defines, but the documentation below keeps them separate so the mental model is clean.

Application code is portable across all combinations — switching is normally a one-define change.


1. What is a Provider?

A Provider is the layer between Horse and the network. It:

  1. Owns the listening socket (or, for host-managed application types, is invoked by the host).
  2. Parses an incoming HTTP request into a TWebRequest (Indy) or an internal shadow-field representation (CrossSocket, future providers).
  3. Calls THorse.Execute(Req, Res), which runs the registered middleware chain and routes.
  4. Serialises the response back to the client.

The same Horse routes, middleware, and THorseRequest / THorseResponse API run under every Provider. You don't rewrite handlers when switching transports.

2. Provider catalogue

The default Provider depends on the compiler:

  • On Delphi (for the Console / VCL / Daemon Application types) the default is Indy (TIdHTTPServer via IdHTTPWebBrokerBridge).
  • On FPC (for the Daemon / HTTPApplication / LCL Application types) the default is FreePascal's fphttpserver library.
  • The optional CrossSocket Provider replaces both defaults with a cross-compiler async transport.
ProviderCompiler defineStatusDelphiLazarus
Indy(none on Delphi)Default for Delphi self-hostedn/a
fphttpserver(none on FPC)Default for FPC self-hostedn/a
horse-provider-crosssocketHORSE_CROSSSOCKETOptional, external package
horse-provider-mormotHORSE_PROVIDER_MORMOTOptional, external package
horse-provider-icsHORSE_PROVIDER_ICSOptional, external package (Delphi: Windows + Linux64/macOS)
HTTP.sys (Horse.Provider.HttpSys)HORSE_PROVIDER_HTTPSYSBuilt into Horse (Windows kernel-mode)
epoll (Horse.Provider.Epoll)HORSE_PROVIDER_EPOLLBuilt into Horse (Linux async event loop)
IOCP (Horse.Provider.IOCP)HORSE_PROVIDER_IOCPBuilt into Horse (Windows async I/O completion ports)
🆕 gRPCHORSE_GRPC / Manual registrationOptional, built-in (gRPC and native HTTP/2 h2c)

What library does the HTTP work, per Application type? This is the deciding question — and it's not always Indy. The unifying abstraction across every row is Web.HTTPApp.TWebRequest on Delphi or fpHTTP.TRequest on FPC; below that, the concrete library differs.

Application typeCompilerTransport libraryIndy?
Console / VCL / DaemonDelphiIndy (TIdHTTPServer + IdHTTPWebBrokerBridge)
Daemon / HTTPApplication / LCLFPCfphttpserver
Any self-hosted + HORSE_CROSSSOCKETEitherDelphi-Cross-Socket
Any self-hosted + HORSE_PROVIDER_MORMOTEithermORMot2 (THttpServer / THttpApiServer)
Self-hosted + HORSE_PROVIDER_ICSDelphi (Windows / Linux64 / macOS)OverbyteICS (THttpServer / TSslHttpServer)
Self-hosted + HORSE_PROVIDER_HTTPSYSWindows (Delphi / FPC)Windows http.sys (httpapi.dll, kernel-mode)
Self-hosted + HORSE_PROVIDER_EPOLLEitherepoll (Linux kernel epoll API)
Self-hosted + HORSE_PROVIDER_IOCPWindows (Delphi / FPC)Windows IOCP (Winsock2 Completion Ports)
Apache moduleEitherApache httpd (via Web.HTTPApp.TApacheRequest / mod_horse)
ISAPIDelphiIIS (via Web.HTTPApp.TISAPIRequest)
CGIDelphiWeb server's CGI runner (via Web.HTTPApp.TCGIRequest)
FastCGIFPCfpFCGI library (talks to web server)

Indy (Delphi default for self-hosted)

The default transport on Delphi when no HORSE_* define is set and the Application type is Console, VCL, or Daemon. Indy ships inside the Horse repository — no extra boss install needed. It uses a thread-per-connection model: one OS thread per accepted client, managed by Indy's thread pool. The unit IdHTTPWebBrokerBridge bridges Indy's TIdHTTPServer to the Web.HTTPApp.TWebRequest abstraction that Horse middleware sees.

Properties:

  • Cross-platform (Windows, Linux, macOS) — Indy itself does the heavy lifting.
  • Trivial deployment: copy the executable, run it.
  • SSL via OpenSSL DLLs placed next to the binary.
  • Scaling ceiling: typically a few hundred to ~1 000 concurrent connections before thread-pool exhaustion or scheduler pressure starts showing.

Pick this when: you have at most a few hundred concurrent clients, deployment is "just run the binary", and you don't need long-polling / SSE / WebSockets at scale.

fphttpserver (FPC default for self-hosted)

On FPC builds (Lazarus), the default self-hosted Provider is FreePascal's own fphttpserver library — not Indy. The FPC provider units (Horse.Provider.FPC.Daemon, Horse.Provider.FPC.HTTPApplication, Horse.Provider.FPC.LCL) uses fphttpserver, fpHTTP, httpdefs and present incoming requests as TRequest / TResponse from HTTPDefs.

Properties:

  • Same thread-per-connection model as Indy, but using FreePascal's RTL.
  • Cross-platform on the FPC targets (Linux, Windows, macOS, BSD).
  • No Indy dependency — keeps FPC builds free of the OpenSSL DLL deployment story.
  • SSL via OpenSSL through Synapse / fpHTTPClient SSL handlers (provider-dependent).

Pick this when: you're on FPC and you want the lightest possible FPC-native build with no extra packages.

CrossSocket (optional)

horse-provider-crosssocket replaces the Indy transport with Delphi-Cross-Socket: an async I/O library that uses IOCP on Windows, epoll on Linux, and kqueue on macOS — the same primitives nginx and Node.js use.

boss install horse-provider-crosssocket

In your project's Conditional Defines: HORSE_CROSSSOCKET. Your code stays the same.

What changes vs. Indy:

IndyCrossSocket
ConcurrencyOne thread per connectionFixed IO-thread pool (CPUCount*2+1, e.g. 9 on a 4-core, 17 on an 8-core)
Idle keep-alive costOne thread per idle connectionOne epoll/IOCP handle — negligible
Per-request allocationNew THorseRequest/THorseResponsePre-warmed object pool (32 contexts, scales to 512)
Scaling ceiling~1 000 concurrent on commodity hardware10 000+ concurrent on the same hardware
Linux deploymentIndy works but isn't its primary platformFirst-class — epoll is the native Linux async primitive
Pre-pipeline request validationNoneURL length, header limits, request-smuggling guards, method allowlist
Object pool, zero-allocation hot pathNoYes

Pick CrossSocket when:

  • You expect more than a few hundred concurrent clients.
  • You deploy on Linux (especially in containers / Docker).
  • You serve long-polling, server-sent events, or many idle keep-alive connections.
  • You want enforced size / smuggling protections at the transport layer.

CrossSocket and Indy are drop-in alternatives for the same Horse codebase. The same middleware (Horse.CORS, Jhonson, JWT, logger, etc.) works on both.

For configuration (TLS certificates, body-size limits, IO thread count, mTLS), see the provider's own documentation. A one-way + mutual-TLS integration test ships in the provider's tests/ (HorseCSTLSTestServer / …Client, see tests/TLS-TESTS.md).

Installation

horse-provider-crosssocket pulls Delphi-Cross-Socket through Boss.

If install manually:

  1. Clone horse-provider-crosssocket.
  2. Clone Delphi-Cross-Socket — two options:
    • Recommended: upstream winddriver/Delphi-Cross-Socket. Tracks the maintainer's release cadence; receives upstream improvements as soon as they land.
    • Supported alternative: the fork release freitasjca/Delphi-Cross-Socket v1.0.3, which bundles CnPack as a vendored subtree and adds the mTLS server-mode APIs (SetCACertificate(File) + SetVerifyPeer(Boolean)). One clone instead of two, at the cost of lagging behind upstream commits between fork syncs. Pick this if you need mTLS server mode or prefer the single-dependency convenience.
  3. For the upstream path only, also clone cnpack/cnvcl and add Source/Common + Source/Crypto to the search path. The fork bundles these files already.
  4. Add all the resulting paths to your project's Search-path field — see horse-provider-crosssocket README for the full three-path runbook (upstream, fork, and Boss-for-Horse-only).

Why "supported alternative" and not "deprecated"? The fork remains actively maintained for users who want bundled CnPack convenience or need mTLS today. The trade-off is straightforward: upstream winddriver/Delphi-Cross-Socket has a higher commit cadence than any fork can keep up with, so the fork inevitably lags. Three previously-fork-only bug fixes (PATCH-IOCP-1 shutdown cascade, the zero-body response parser hang, the _OnBodyEnd nil-guard) have already been merged upstream as of 2026-Q2 — only the mTLS additions remain genuinely fork-exclusive. An upstream mTLS PR is in preparation; once merged, the fork's only remaining value will be the bundled CnPack convenience.

mORMot2 (optional)

horse-provider-mormot replaces the Indy transport with mORMot2: a mature, high-performance library that uses IOCP on Windows and epoll on Linux — the same kernel primitives as CrossSocket. mORMot2 manages its own fixed thread pool (default 32 threads) so no THorseWorkerPool is needed.

boss install horse-provider-mormot

In your project's Conditional Defines: HORSE_PROVIDER_MORMOT. Your code stays the same.

Three server backends. The provider can host any of mORMot2's HTTP servers, selected via THorseMormotConfig.ServerKind: mskThreadPool (THttpServer, the default — one thread per concurrent request), mskAsync (THttpAsyncServer, a non-blocking IOCP/epoll/kqueue event loop that scales past thread-per-request), or mskHttpApi (THttpApiServer, Windows http.sys kernel-mode HTTP — Windows only). Switch at runtime (Cfg.ServerKind := mskAsync before THorse.ListenWithConfig) or set a project-wide define (HORSE_MORMOT_ASYNC, or HORSE_MORMOT_HTTPAPI on Windows) to flip the default. With none, the default stays THttpServer — unchanged behaviour. mskHttpApi registers http://+:<port>/ with http.sys, which needs Administrator rights or a one-time netsh http add urlacl. See the provider's own documentation for details.

What changes vs. Indy:

IndymORMot2
ConcurrencyOne thread per connectionFixed thread pool (default 32, configurable)
Idle keep-alive costOne thread per idle connectionThread only runs when data is ready
Per-request allocationNew THorseRequest/THorseResponsePre-warmed object pool (32 contexts, scales to 512)
Scaling ceiling~1 000 concurrent on commodity hardware10 000+ concurrent on the same hardware
Compiler supportDelphi XE7+Delphi 7 through 12.3 Athens, FPC 3.2+
http.sys (Windows)THttpApiServer — kernel-mode HTTP, zero code change
External dependencyIndy (bundled)mORMot2 (add to search path)

What changes vs. CrossSocket:

CrossSocketmORMot2
Thread poolTHorseWorkerPool (4–64 Horse threads)Built-in (default 32 threads)
External dependencyDelphi-Cross-Socket + CnPackmORMot2 + (Delphi only) precompiled static .obj blobs for zlib/OpenSSL/SQLite
Boss-installable✔ both deps❌ — mORMot2 must be cloned manually and added to the search path
Older Delphi supportDelphi 10.2+Delphi 7+
http.sys (Windows kernel-mode HTTP)✔ swap THttpServer for THttpApiServer
mTLS server mode✔ via freitasjca/Delphi-Cross-Socket >= 1.0.3✔ via THorseMormotConfig.SSLVerifyPeer + SSLCACertFile (socket backends)

Pick mORMot2 when:

  • You need Delphi 7 / XE / XE2 support.
  • You want no third-party library dependency for standard HTTP (pure Pascal).
  • You want Windows http.sys kernel-mode HTTP (THttpApiServer).
  • You prefer a 15+-year production-proven codebase.

Pick CrossSocket when:

  • You prefer native async IOCP/epoll control.
  • Your project already depends on Delphi-Cross-Socket.

Both providers use IOCP/epoll and a context object pool, so throughput is comparable under typical workloads.

For configuration (ServerKind, ThreadPool, MaxBodyBytes, DrainTimeoutMs, ServerBanner, and the SSL* TLS fields below) and application-type wrappers (VCL, Windows Service, Linux daemon), see the provider's own documentation.

HTTPS / TLS. THorseMormotConfig carries the same TLS surface as the CrossSocket / ICS providers; the provider builds a mORMot TNetTlsContext and passes it to THttpServerSocketGeneric.WaitStarted:

Cfg := THorseMormotConfig.Default;
Cfg.SSLEnabled     := True;
Cfg.SSLCertFile    := 'server.crt';
Cfg.SSLPrivKeyFile := 'server.key';
Cfg.SSLCACertFile  := 'ca.crt';     // mutual TLS
Cfg.SSLVerifyPeer  := True;          // require + verify a client certificate
THorseProviderMormot.ListenWithConfig(9443, Cfg);

TLS applies to the socket backends (mskThreadPool, mskAsync); on mskHttpApi the certificate binds at the OS level (netsh http add sslcert), so SSLEnabled raises a clear error there. A one-way + mutual-TLS integration test ships in the provider's tests/ (HorseMormotTLSTestServer / …Client, see tests/TLS-TESTS.md).

mORMot2 installation — mORMot2 is not available via boss install. Clone synopse/mORMot2 and add <mORMot2>/src, <mORMot2>/src/core, <mORMot2>/src/net to the compiler search path.

ICS (optional)

horse-provider-ics replaces the Indy transport with OverbyteICS (THttpServer / TSslHttpServer). Its distinctive value is ICS's modern OpenSSL 3.x / 4.x stack — TLS 1.3, SNI, mutual TLS, security-level controls — making it the choice when you need up-to-date server-side TLS. ICS sockets are single-thread-affine, so the provider offloads the pipeline to a worker pool and marshals the response back to the loop thread.

In your project's Conditional Defines: HORSE_PROVIDER_ICS. Your code stays the same.

var
  Cfg: THorseICSConfig;
begin
  Cfg := THorseICSConfig.Default;
  Cfg.SSLEnabled     := True;
  Cfg.SSLCertFile    := 'server.pem';
  Cfg.SSLPrivKeyFile := 'server.key';
  Cfg.SSLCAFile      := 'ca.pem';   // mutual TLS
  Cfg.SSLVerifyPeer  := True;
  THorseProviderICS.ListenWithConfig(9443, Cfg);
end;

Scope: Delphi only — Windows and POSIX (Linux64 / macOS). ICS's POSIX layer (Ics.Posix.WinTypes + Ics.Posix.PXMessages) supplies the same TIcsWndControl message loop on Linux/macOS, so the worker-pool marshal-back and OpenSSL TLS carry over with no code change; OpenSSL ships as .so on Linux. For a Linux service use HORSE_APPTYPE_DAEMON + THorseICSLinuxDaemonApp.Run (SIGTERM/SIGINT handlers + blocking Listen). A Lazarus/FPC port is not viable — ICS's POSIX support rides the Delphi POSIX RTL and ICS compiles OpenSSL out under FPC. Selecting HORSE_PROVIDER_ICS under FPC is a compile-time FATAL.

Installation

ICS is not Boss-installable. Install OverbyteICS by following the official ICS instructions: download or clone ICS (v9.x) and add its Source/ folder to your project's search path. The OpenSSL DLLs ship with the ICS distribution. Then clone horse-provider-ics and add its src/ to the search path. See the provider's own documentation for the full setup, the A–K integration test suite, and known limitations (uploads must send Content-Length; keep-alive is disabled in v1). A dedicated one-way + mutual-TLS test ships alongside (HorseICSTLSTestServer / …Client, see tests/TLS-TESTS.md) — the most important test for ICS, whose distinctive value is its OpenSSL stack.

HttpSys (optional, built-in)

Unlike CrossSocket / mORMot / ICS, the HttpSys Provider is part of Horse itself — the Horse.Provider.HttpSys unit ships with the framework. It binds directly to Windows' HTTP Server API (httpapi.dll), i.e. the http.sys kernel-mode HTTP stack that also backs IIS. There is no external library to install and no Boss dependency — it's pure OS.

In your project's Conditional Defines: HORSE_PROVIDER_HTTPSYS. Your code stays the same.

Scope: Windows only (http.sys is a Windows kernel facility), on both Delphi and FPC/Lazarus. Selecting it on a non-Windows target is a compile-time error. It is mutually exclusive with the CrossSocket / mORMot / ICS Providers — exactly one transport Provider per build (Horse.pas enforces this with {$MESSAGE FATAL}).

Why http.sys? Kernel-mode request routing, response caching, and the ability to share port 80/443 with IIS and other http.sys apps on the same machine; TLS is configured at the OS level (Windows certificate store) rather than in your process.

Installation & runtime requirements

No install — it's built in. Two OS-level requirements stem from http.sys being machine-wide:

  • URL reservation. Binding any host other than localhost, or a privileged port, requires a one-time URL ACL or Administrator rights:
    netsh http add urlacl url=http://+:9000/ user=Everyone
    
  • HTTPS is bound to a certificate in the Windows store, not a .pem file:
    netsh http add sslcert ipport=0.0.0.0:9443 certhash=<thumbprint> appid={<guid>}
    

This is the same model as mORMot's mskHttpApi backend (THttpApiServer) — both ride http.sys; HttpSys is the standalone, dependency-free way to get it.

Future providers

The hybrid-interface architecture (IHorseRawRequest / IHorseRawResponse) introduced by CrossSocket makes it straightforward to add additional transports. For nghttp2 see building-a-new-provider.md.


3. Application types — self-hosted

Self-hosted application types run the Provider you chose. The Provider owns the socket; your binary owns the process.

Application typeCompiler defineDelphiLazarus
Console (default)(none)
VCLHORSE_VCL
Daemon — Windows ServiceHORSE_DAEMONn/a
Daemon — Linux daemon (systemd)HORSE_DAEMON
LCL (Lazarus GUI)HORSE_LCL
HTTPApplication (FPC)(FPC default)

NoteHORSE_DAEMON is a unified application type: the produced binary is a Windows Service (Vcl.SvcMgr.TService + SCM) on Windows and a daemon (signal(SIGTERM) + systemd) on Linux. "Daemon" is the Unix-native term; Windows has no native equivalent word, so the define name is borrowed cross-platform.

Console — the default

The simplest case. Write a .dpr with {$APPTYPE CONSOLE}, call THorse.Listen(port), run the binary. Works on Windows, Linux, and macOS. Most projects fit this shape.

VCL — server inside a desktop app

Useful when your Delphi application has a UI and you want it to expose an HTTP control surface (a remote-control endpoint, an embedded admin panel). With HORSE_VCL defined, Horse starts the Provider on a background thread; the VCL main thread stays free for the form / message loop. Indy-only today; CrossSocket VCL support is architecturally possible but not currently expressible via the defines.

Daemon — Windows Service or Linux daemon (systemd)

HORSE_DAEMON is a unified application type. The same .dpr compiles for both target OSes; the unit's {$IFDEF MSWINDOWS} branch selects the host-integration path at compile time.

  • Windows: Horse wires Listen / Stop into the Service Control Manager via Vcl.SvcMgr.TService. Combined with a service wrapper, you get sc start MyService / sc stop MyService integration.
  • Linux: Horse installs POSIX signal handlers for SIGTERM / SIGINT (the standard systemd shutdown signals) and ignores SIGPIPE. Combined with a .service unit file, systemctl start/stop MyService works identically.

Same transport (Indy on Delphi, fphttpserver on FPC, or CrossSocket / mORMot2 when their Provider is also defined) underneath in either case. See the Deployment Cheatsheet for both lifecycle templates side by side.

LCL — Lazarus GUI application

The Lazarus counterpart to VCL. With HORSE_LCL defined, the Provider runs alongside the Lazarus message loop on FPC. Used when a Lazarus desktop app needs to expose HTTP.

HTTPApplication — FPC native HTTP application

A standalone FPC-only application type that uses FreePascal's fpHTTP server scaffolding. Used when you want a pure-FPC build that integrates with Lazarus tooling without involving Indy.


4. Host-managed application types

When the application type is host-managed, the host process (Apache httpd, IIS, the web server) owns the socket and hands a pre-parsed request to your Delphi/FPC code. The Providers from §2 are not used at allno Indy, no fphttpserver, no CrossSocket. Instead the request abstraction is provided by:

  • Delphi: built-in Web.HTTPApp subclasses — TApacheRequest (Apache module), TISAPIRequest (IIS), TCGIRequest (CGI). Horse uses Web.WebBroker, Web.ApacheApp / Web.Win.ISAPIApp / Web.CGIApp accordingly.
  • FPC: FreePascal's fpFCGI library for FastCGI (the FCGI process listens on a socket for the web server, then dispatches via fpHTTP).

The host is the transport. Horse middleware still sees the same TWebRequest / TRequest it always sees — that's the unifying abstraction.

Application typeCompiler defineBuild artefactHosted byDelphiLazarus
Apache moduleHORSE_APACHE.so / .dll Apache moduleApache httpd via mod_horse
ISAPIHORSE_ISAPIISAPI extension .dllIIS
CGIHORSE_CGIStandalone executableWeb server (one process per request)
FastCGIHORSE_FCGIStandalone executableWeb server (persistent FCGI pool)

Pick one of these when: you already have an Apache / IIS / nginx deployment and want to integrate Horse without standing up a separate process. The host handles SSL, gzip, virtual hosting, and you focus on the application.

Trade-offs:

  • Lifecycle controlled by the host — no graceful shutdown hooks, no event loop you own.
  • Build / deploy more involved than a single executable.
  • The Provider catalogue above doesn't apply — features like CrossSocket's async I/O, request smuggling guards, or object pool are architecturally absent because there is no socket to own.

5. Compatibility matrix

Provider × Application type — which combinations are currently expressible (and architecturally compatible)? The Delphi-only Application types are in the top half; FPC-only ones in the bottom; host-managed across the right.

Console (D)VCL (D)Daemon (D)Daemon (FPC)LCL (FPC)HTTPApplication (FPC)ApacheISAPICGIFCGI
Indy (Delphi default)n/an/an/an/an/an/an/a
fphttpserver (FPC default)n/an/an/an/an/an/an/a
CrossSocket (HORSE_PROVIDER_CROSSSOCKET)
mORMot2 (HORSE_PROVIDER_MORMOT)
ICS (HORSE_PROVIDER_ICS) (Delphi; Windows + Linux64/macOS)n/an/an/a
HttpSys (HORSE_PROVIDER_HTTPSYS) (Windows; built-in)
Host-managed (Apache/ISAPI/CGI/FCGI)n/an/an/an/an/an/a
ICS (HORSE_PROVIDER_ICS) (Delphi; Windows + Linux64/macOS)n/an/an/a

Legend:

  • — supported and expressible with the current defines. Since PATCH-HORSE-2, every CrossSocket × Application-type cell is supported via the cross-product convenience units in horse-provider-crosssocket (e.g. Horse.Provider.CrossSocket.VCL, …Daemon, …FPC.Daemon, …FPC.LCL, …FPC.HTTPApplication). mORMot2 ships the matching cross-product set in horse-provider-mormot: Horse.Provider.Mormot (Console default), …Mormot.VCL, …Mormot.Daemon (Windows TService + POSIX runner in one unit), …Mormot.FPC.Daemon, …Mormot.FPC.LCL, …Mormot.FPC.HTTPApplication.
  • — architecturally impossible. Apache / ISAPI / CGI / FCGI own the socket themselves; an async self-hosted transport like CrossSocket or mORMot2 can't coexist.
  • n/a — meaningless combination. Indy doesn't run on FPC; fphttpserver doesn't run on Delphi; host-managed application types don't use a self-hosted Provider; self-hosted types don't run under a host's lifecycle.

PATCH-HORSE-1 in Horse.pas enforces the ❌ cells at compile time with {$MESSAGE FATAL} so misconfigured projects fail fast instead of silently picking the wrong code path.


6. Selecting your defines

The selection happens at compile time via Project Options → Conditional Defines. PATCH-HORSE-2 splits the defines into three explicit namespaces — one per axis — and the chain composes them.

The three namespaces

AxisPrefixMeaning
A · ProviderHORSE_PROVIDER_*HTTP transport library — Indy (Delphi default), fphttpserver (FPC default), CrossSocket, mORMot2, ICS (Delphi/Windows), HttpSys (Windows http.sys, built-in)
B · Application typeHORSE_APPTYPE_*Binary lifecycle shape — Console (default), VCL, Daemon, LCL, HTTPApplication
C · Host-managed runtimeHORSE_HOST_*Web server owns the socket — Apache, ISAPI, CGI, FastCGI

Axis C wins outright when set (no Provider involved). Axes A and B compose freely.

Common combinations

GoalDefine(s) to set
Default — Console + Indy (Delphi)(none)
Default — HTTPApplication + fphttpserver (FPC)(none)
VCL desktop app + IndyHORSE_APPTYPE_VCL
Windows service + IndyHORSE_APPTYPE_DAEMON
Lazarus GUI + fphttpserverHORSE_APPTYPE_LCL
Async high-performance Console + CrossSocketHORSE_PROVIDER_CROSSSOCKET
CrossSocket + VCL (new in PATCH-HORSE-2)HORSE_PROVIDER_CROSSSOCKET + HORSE_APPTYPE_VCL
CrossSocket + Windows service (new in PATCH-HORSE-2)HORSE_PROVIDER_CROSSSOCKET + HORSE_APPTYPE_DAEMON
CrossSocket + Linux daemon (FPC) (new in PATCH-HORSE-2)HORSE_PROVIDER_CROSSSOCKET + HORSE_APPTYPE_DAEMON (on FPC)
CrossSocket + Lazarus LCL (new in PATCH-HORSE-2)HORSE_PROVIDER_CROSSSOCKET + HORSE_APPTYPE_LCL
mORMot2 ConsoleHORSE_PROVIDER_MORMOT
mORMot2 + VCLHORSE_PROVIDER_MORMOT + HORSE_APPTYPE_VCL
mORMot2 + Windows serviceHORSE_PROVIDER_MORMOT + HORSE_APPTYPE_DAEMON (on Windows)
mORMot2 + Linux daemonHORSE_PROVIDER_MORMOT + HORSE_APPTYPE_DAEMON (on Linux)
ICS Console (Windows)HORSE_PROVIDER_ICS
ICS + VCL (Windows)HORSE_PROVIDER_ICS + HORSE_APPTYPE_VCL
ICS + Windows serviceHORSE_PROVIDER_ICS + HORSE_APPTYPE_DAEMON
HttpSys Console (Windows)HORSE_PROVIDER_HTTPSYS
HttpSys + Windows serviceHORSE_PROVIDER_HTTPSYS + HORSE_APPTYPE_DAEMON
Apache moduleHORSE_HOST_APACHE
IIS ISAPI extensionHORSE_HOST_ISAPI
Plain CGIHORSE_HOST_CGI
FastCGIHORSE_HOST_FCGI

Legacy aliases (Horse <3.2 compatibility)

Every old define is automatically translated to the new namespace by an alias block at the top of Horse.pas — every existing .dproj / .lpi continues to compile unchanged.

Legacy defineNew namespaced define
HORSE_CROSSSOCKETHORSE_PROVIDER_CROSSSOCKET
HORSE_VCLHORSE_APPTYPE_VCL
HORSE_DAEMONHORSE_APPTYPE_DAEMON
HORSE_LCLHORSE_APPTYPE_LCL
HORSE_APACHEHORSE_HOST_APACHE
HORSE_ISAPIHORSE_HOST_ISAPI
HORSE_CGIHORSE_HOST_CGI
HORSE_FCGIHORSE_HOST_FCGI
HORSE_NOPROVIDERunchanged (escape hatch)

New projects should prefer the namespaced names — they're self-documenting and they make the three-axis model visible in the project's conditional defines.

Horse.pas resolves the active selection in a two-stage {$IFDEF} chain — host-managed first (Axis C wins outright), then Provider × Application-type composition. There is no runtime switching. If you want to deploy the same code as both a console binary and an Apache module, build twice with different defines.

7. What does NOT change when you switch

  • Your route declarations.
  • Your handler bodies.
  • Your middleware.
  • The THorseRequest / THorseResponse API your code uses.
  • The supported Delphi / FPC versions (within each Provider's range — see Compiler Support).

The Provider and Application type are intentionally swappable. If you keep your code transport-neutral (and Horse encourages this), you can A/B test Indy vs CrossSocket simply by toggling the define and rebuilding.


8. Running CrossSocket as each Application type

HORSE_CROSSSOCKET cannot combine with HORSE_VCL / HORSE_DAEMON / HORSE_LCL / HORSE_ISAPI / HORSE_APACHE / HORSE_CGI / HORSE_FCGI at compile time (PATCH-HORSE-1 enforces this). To achieve the runtime shape of each of those Application types while using CrossSocket as the transport, define only HORSE_CROSSSOCKET and add the small shape-specific shell yourself.

The common pattern across every shape:

  1. Define only HORSE_CROSSSOCKET — no other HORSE_* provider define.
  2. Pick the right project type for the desired shape (Console / VCL Forms / Lazarus / Service Application / …).
  3. Drive THorse.Listen from the right lifecycle event for that shape.
  4. Call THorse.StopListen on shutdown so CrossSocket drains active requests (SEC-30) before the process exits.

CrossSocket's Listen automatically picks between blocking and non-blocking behaviour based on IsConsole:

  • IsConsole = True (console binary) → Listen blocks the calling thread until StopListen is called.
  • IsConsole = False (VCL / LCL / TService / …) → Listen starts the IO threads and returns immediately; the calling thread is free to drive a GUI message loop or service control loop. Call StopListen from a teardown handler.

8.1 Console (Delphi)

The default and simplest case. THorse.Listen blocks the main thread; StopListen from a Ctrl-C handler unblocks it.

program MyServer;

{$APPTYPE CONSOLE}                 // → IsConsole = True

uses
  Winapi.Windows, Horse;

function CtrlHandler(dwCtrlType: DWORD): BOOL; stdcall;
begin
  case dwCtrlType of
    CTRL_C_EVENT, CTRL_BREAK_EVENT, CTRL_CLOSE_EVENT,
    CTRL_SHUTDOWN_EVENT:
      begin
        THorse.StopListen;          // drain → Listen returns
        Result := True;
      end;
  else
    Result := False;
  end;
end;

begin
  SetConsoleCtrlHandler(@CtrlHandler, True);

  THorse.Get('/ping',
    procedure(Req: THorseRequest; Res: THorseResponse)
    begin Res.Send('pong'); end);

  THorse.Listen(9000);             // blocks
end.

Define: HORSE_PROVIDER_CROSSSOCKET (or the legacy alias HORSE_CROSSSOCKET). Project type: Console Application.

8.2 VCL desktop app (Delphi)

A VCL Forms project with an embedded HTTP server. The VCL main thread runs the message loop; CrossSocket runs on its own IO threads.

unit Main.Form;

interface

uses
  Vcl.Forms, System.Classes, Horse;

type
  TfrmMain = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  end;

implementation

{$R *.dfm}

procedure TfrmMain.FormCreate(Sender: TObject);
begin
  THorse.Get('/ping',
    procedure(Req: THorseRequest; Res: THorseResponse)
    begin Res.Send('pong'); end);

  // IsConsole = False in a VCL app → Listen starts the IO threads
  // and returns immediately. The VCL message loop keeps the form alive.
  THorse.Listen(9000);
end;

procedure TfrmMain.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  THorse.StopListen;               // graceful drain before the form closes
end;

end.

Defines: HORSE_PROVIDER_CROSSSOCKET + HORSE_APPTYPE_VCL (PATCH-HORSE-2). Project type: VCL Forms Application. Do not add {$APPTYPE CONSOLE} to the .dpr — that would force IsConsole = True and Listen would block the main thread, freezing the UI.

Tip: the optional convenience base class TfrmHorseVCLHost (in Horse.Provider.CrossSocket.VCL) pre-wires FormCreateTHorse.Listen(Port) and FormCloseTHorse.StopListen. Inherit from it instead of writing the wiring above by hand.

8.3 Linux daemon (Delphi cross-compiled to Linux)

A standalone Linux binary supervised by systemd. The daemon shape is provided by systemd, not by Horse.

program MyDaemon;

{$APPTYPE CONSOLE}                 // → IsConsole = True; Listen blocks

uses
  {$IFDEF LINUX} Posix.Signal, {$ENDIF}
  Horse;

{$IFDEF LINUX}
procedure HandleSignal(ASignal: Integer); cdecl;
begin
  THorse.StopListen;               // SIGTERM from systemd → drain → exit
end;
{$ENDIF}

begin
  {$IFDEF LINUX}
  signal(SIGTERM, @HandleSignal);
  signal(SIGINT,  @HandleSignal);
  {$ENDIF}

  THorse.Get('/ping',
    procedure(Req: THorseRequest; Res: THorseResponse)
    begin Res.Send('pong'); end);

  THorse.Listen(9000);
end.

systemd unit at /etc/systemd/system/myhorse.service:

[Unit]
Description=My Horse Server (CrossSocket)
After=network.target

[Service]
Type=simple
User=horse
WorkingDirectory=/opt/myhorse
ExecStart=/opt/myhorse/MyDaemon
ExecStop=/bin/kill -TERM $MAINPID
Restart=on-failure
RestartSec=5s
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload && sudo systemctl enable --now myhorse

Defines: HORSE_PROVIDER_CROSSSOCKET + HORSE_APPTYPE_DAEMON (PATCH-HORSE-2). Project target: Linux64.

Tip: the optional convenience runner THorseCrossSocketLinuxDaemonApp.Run(@SetupRoutes, Port) (in Horse.Provider.CrossSocket.Daemon — the same unit as §8.4's THorseCrossSocketService, just the non-Windows branch) installs signal(SIGTERM/SIGINT) handlers, ignores SIGPIPE, and calls THorse.Listen for you. The whole program body collapses to a single call.

Note on HORSE_APPTYPE_DAEMON on Delphi: the same define means "Windows Service" when building for Windows and "Linux daemon" when building for Linux. Horse.Provider.CrossSocket.Daemon.pas carries both lifecycle helpers (THorseCrossSocketService under {$IFDEF MSWINDOWS}, THorseCrossSocketLinuxDaemonApp under {$ELSE}). This mirrors the cross-platform behaviour of the Indy-based Horse.Provider.Daemon.pas. "Daemon" is the intent (OS-supervised long-running process); the OS-specific machinery is picked by the build target.

8.4 Windows Service (Delphi)

A Delphi Service Application that drives CrossSocket. The Windows SCM start/stop hooks call THorse.Listen / StopListen on a worker thread so the SCM message pump stays responsive.

unit MyHorseSvc;

interface

uses
  System.SysUtils, System.Classes, Vcl.SvcMgr, Horse;

type
  TMyHorseService = class(TService)
    procedure ServiceStart(Sender: TService; var Started: Boolean);
    procedure ServiceStop(Sender: TService; var Stopped: Boolean);
  private
    FListenerThread: TThread;
  end;

var
  MyHorseService: TMyHorseService;

implementation

{$R *.dfm}

procedure TMyHorseService.ServiceStart(Sender: TService; var Started: Boolean);
begin
  THorse.Get('/ping',
    procedure(Req: THorseRequest; Res: THorseResponse)
    begin Res.Send('pong'); end);

  // IsConsole = False in a service app → Listen returns immediately
  // and CrossSocket runs on its own IO threads. But we also have to
  // keep the *service* call non-blocking so the SCM gets its ack — so
  // we run Listen on a dedicated thread that we own.
  FListenerThread := TThread.CreateAnonymousThread(
    procedure begin THorse.Listen(9000); end);
  FListenerThread.FreeOnTerminate := False;
  FListenerThread.Start;

  Started := True;
end;

procedure TMyHorseService.ServiceStop(Sender: TService; var Stopped: Boolean);
begin
  THorse.StopListen;                // drain
  if Assigned(FListenerThread) then
  begin
    FListenerThread.WaitFor;
    FreeAndNil(FListenerThread);
  end;
  Stopped := True;
end;

end.

Defines: HORSE_PROVIDER_CROSSSOCKET + HORSE_APPTYPE_DAEMON (PATCH-HORSE-2). Project type: Service Application (File → New → Other → Service Application).

Tip: the optional convenience base class THorseCrossSocketService (in Horse.Provider.CrossSocket.Daemon) pre-wires ServiceStart to spawn the worker thread for Listen and ServiceStop to drain + join cleanly. Inherit from it instead of writing the wiring above by hand.

Install / uninstall via the standard SCM verbs. Run from an elevated (Administrator) Command Prompt/install, /uninstall, sc start, and sc stop all go through the Service Control Manager, which rejects non-elevated tokens with EOSError ... Code: 5. Access is denied. even when the user is in the Administrators group (UAC filters the token).

MyHorseServer.exe /install
sc start MyHorseService
sc stop  MyHorseService
MyHorseServer.exe /uninstall

Simpler alternative if you don't want to write the TService wrapper: build a Console binary (§8.1) and register it as a Windows service via NSSM. NSSM sends Ctrl+Break on stop; the CtrlHandler from §8.1 catches it and calls StopListen.

8.5 Linux daemon (FPC / Lazarus)

Same shape as §8.3 but built with FPC. The FPC default Provider on a no-define build is fphttpserver; defining HORSE_CROSSSOCKET swaps it for CrossSocket and the binary's HTTP layer becomes async.

program MyDaemon;

{$MODE DELPHI}{$H+}
{$APPTYPE CONSOLE}                 // → IsConsole = True; Listen blocks

uses
  {$IFDEF UNIX} BaseUnix, {$ENDIF}
  SysUtils, Horse;

{$IFDEF UNIX}
procedure HandleSignal(ASignal: cint); cdecl;
begin
  THorse.StopListen;
end;
{$ENDIF}

procedure GetPing(Req: THorseRequest; Res: THorseResponse);
begin
  Res.Send('pong');
end;

begin
  {$IFDEF UNIX}
  fpSignal(SIGTERM, @HandleSignal);
  fpSignal(SIGINT,  @HandleSignal);
  {$ENDIF}

  THorse.Get('/ping', @GetPing);   // note the @ — FPC needs it
  THorse.Listen(9000);
end.

Defines: HORSE_PROVIDER_CROSSSOCKET + HORSE_APPTYPE_DAEMON (PATCH-HORSE-2). Build with fpc or lazbuild, target Linux.

Tip: the optional convenience runner THorseCrossSocketDaemonApp.Run(@SetupRoutes, Port) (in Horse.Provider.CrossSocket.FPC.Daemon) installs fpSignal(SIGTERM) / SIGINT handlers and calls THorse.Listen for you. The whole program body collapses to a single line: THorseCrossSocketDaemonApp.Run(@SetupRoutes, 9000);.

systemd unit is identical to §8.3.

8.6 Lazarus LCL desktop app (FPC)

The Lazarus counterpart to §8.2. Lazarus GUI app embedding a CrossSocket HTTP server.

unit Main.Form;

{$MODE DELPHI}{$H+}

interface

uses
  Classes, SysUtils, Forms, Horse;

type
  TfrmMain = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
  end;

implementation

{$R *.lfm}

procedure GetPing(Req: THorseRequest; Res: THorseResponse);
begin
  Res.Send('pong');
end;

procedure TfrmMain.FormCreate(Sender: TObject);
begin
  THorse.Get('/ping', @GetPing);
  THorse.Listen(9000);             // IsConsole = False → returns immediately
end;

procedure TfrmMain.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
  THorse.StopListen;
end;

end.

Defines: HORSE_PROVIDER_CROSSSOCKET + HORSE_APPTYPE_LCL (PATCH-HORSE-2). Project type: Lazarus Application (a normal GUI project). Do not use {$APPTYPE CONSOLE}.

Tip: the optional convenience base class TfrmHorseLCLHost (in Horse.Provider.CrossSocket.FPC.LCL) is the Lazarus mirror of TfrmHorseVCLHost — same Port / OnHorseListen / auto-wired FormCreate/FormClose.

8.7 FPC HTTPApplication

The FPC HTTPApplication shape is a standalone FPC executable that traditionally uses fphttpapp.Application.Run to own the main loop. With CrossSocket, the loop is owned by CrossSocket's IO threads instead — so the structure collapses to the same shape as §8.5 (a console-shape FPC binary that calls THorse.Listen).

program MyHttpApp;

{$MODE DELPHI}{$H+}
{$APPTYPE CONSOLE}

uses
  {$IFDEF UNIX} BaseUnix, {$ENDIF}
  SysUtils, Horse;

{$IFDEF UNIX}
procedure HandleSignal(ASignal: cint); cdecl;
begin
  THorse.StopListen;
end;
{$ENDIF}

procedure GetPing(Req: THorseRequest; Res: THorseResponse);
begin
  Res.Send('pong');
end;

begin
  {$IFDEF UNIX}
  fpSignal(SIGTERM, @HandleSignal);
  fpSignal(SIGINT,  @HandleSignal);
  {$ENDIF}

  THorse.Get('/ping', @GetPing);
  THorse.Listen(9000);             // CrossSocket owns the loop
end.

Define: HORSE_PROVIDER_CROSSSOCKET (no HORSE_APPTYPE_* — HTTPApplication is the FPC default for self-hosted; PATCH-HORSE-2 resolves the chain to the existing console-shape unit). The convenience class THorseCrossSocketHTTPApp.Run(@SetupRoutes, Port) (in Horse.Provider.CrossSocket.FPC.HTTPApplication) delegates to the same signal-handler runner as §8.5.

If you actually need to keep the fphttpapp.TFPHTTPApplication lifecycle (because some library you depend on expects it), instantiate it but never call Application.Run — let THorse.Listen own the main loop instead. Two competing event loops in the same process is the failure mode PATCH-HORSE-1 explicitly prevents at compile time.


Quick reference

Application type{$APPTYPE CONSOLE}IsConsoleListen blocks?Where to call ListenWhere to call StopListen
Console (Delphi)TrueYesbegin … end.SetConsoleCtrlHandler
VCL (Delphi)FalseNoFormCreateFormClose
Linux daemon (Delphi)TrueYesbegin … end.POSIX SIGTERM handler
Windows Service (Delphi)FalseNoTService.ServiceStart on a worker threadTService.ServiceStop
Linux daemon (FPC)TrueYesbegin … end.fpSignal(SIGTERM, …)
LCL (FPC / Lazarus)FalseNoFormCreateFormClose
FPC HTTPApplicationTrueYesbegin … end.fpSignal(SIGTERM, …)

All seven cases share the same compile-time configuration: define HORSE_CROSSSOCKET only, no other HORSE_* provider define.


9. Running mORMot2 as each Application type

The same IsConsole / lifecycle pattern from §8 applies to mORMot2 — the provider units (Horse.Provider.Mormot.*) wrap mORMot's THttpServer instead of TCrossHttpServer but expose the same Listen / StopListen contract. The four-step recipe is identical:

  1. Define only HORSE_PROVIDER_MORMOT — no other HORSE_* provider define.
  2. Pick the right project type for the desired shape (Console / VCL Forms / Lazarus / Service Application / …).
  3. Drive THorse.Listen from the right lifecycle event for that shape.
  4. Call THorse.StopListen on shutdown so mORMot's active-request counter drains active requests before the process exits.

mORMot's Listen honours the same IsConsole switch as CrossSocket: True → blocks the calling thread until StopListen; False → returns immediately after THttpServer.WaitStarted so the GUI / service / LCL loop owns the main thread.

9.1 Side-by-side with §8 — the only differences

The whole code skeleton in each of §8.1–8.7 ports to mORMot2 by two substitutions:

In §8 (CrossSocket)In the mORMot2 equivalent
HORSE_PROVIDER_CROSSSOCKET defineHORSE_PROVIDER_MORMOT define
Horse.Provider.CrossSocket.{VCL,Daemon,FPC.Daemon,FPC.LCL,FPC.HTTPApplication} convenience helperHorse.Provider.Mormot.{VCL,Daemon,FPC.Daemon,FPC.LCL,FPC.HTTPApplication}

The convenience helpers map one-to-one — the table below is the complete inventory:

ShapeCrossSocket helpermORMot2 helper
Console (§8.1)(none — direct provider use)(none — direct provider use)
VCL desktop (§8.2)TfrmHorseVCLHost in Horse.Provider.CrossSocket.VCLTfrmHorseMormotVCLHost in Horse.Provider.Mormot.VCL
Linux daemon, Delphi (§8.3)THorseCrossSocketLinuxDaemonApp.Run in Horse.Provider.CrossSocket.DaemonTHorseMormotLinuxDaemonApp.Run in Horse.Provider.Mormot.Daemon
Windows Service (§8.4)THorseCrossSocketService (TService base) in Horse.Provider.CrossSocket.DaemonTHorseMormotService (TService base) in Horse.Provider.Mormot.Daemon
Linux daemon, FPC (§8.5)THorseCrossSocketDaemonApp.Run in Horse.Provider.CrossSocket.FPC.DaemonTHorseMormotFPCDaemonApp.Run in Horse.Provider.Mormot.FPC.Daemon
Lazarus LCL (§8.6)TfrmHorseLCLHost in Horse.Provider.CrossSocket.FPC.LCLTfrmHorseMormotLCLHost in Horse.Provider.Mormot.FPC.LCL
FPC HTTPApplication (§8.7)THorseCrossSocketHTTPApp.Run in Horse.Provider.CrossSocket.FPC.HTTPApplicationTHorseMormotHTTPApp.Run in Horse.Provider.Mormot.FPC.HTTPApplication

The same {$IFDEF MSWINDOWS} / {$ELSE} rule that gives HORSE_APPTYPE_DAEMON two meanings under CrossSocket applies to mORMot too: Horse.Provider.Mormot.Daemon.pas carries THorseMormotService (Windows TService) under {$IFDEF MSWINDOWS} and THorseMormotLinuxDaemonApp (POSIX signal-handler runner) under {$ELSE}.

9.2 Minimal example — Console (mORMot2)

program MyMormotServer;

{$APPTYPE CONSOLE}                 // → IsConsole = True

uses
  Winapi.Windows, Horse;           // resolves THorse → THorseProviderMormot

function CtrlHandler(dwCtrlType: DWORD): BOOL; stdcall;
begin
  case dwCtrlType of
    CTRL_C_EVENT, CTRL_BREAK_EVENT, CTRL_CLOSE_EVENT,
    CTRL_SHUTDOWN_EVENT:
      begin
        THorse.StopListen;
        Result := True;
      end;
  else
    Result := False;
  end;
end;

begin
  SetConsoleCtrlHandler(@CtrlHandler, True);

  THorse.Get('/ping',
    procedure(Req: THorseRequest; Res: THorseResponse)
    begin Res.Send('pong'); end);

  THorse.Listen(9000);             // blocks
end.

Define: HORSE_PROVIDER_MORMOT. Project type: Console Application. No CrossSocket-style convenience runner here — Console is direct provider use, identical to §8.1 except for the resolved unit behind THorse.

9.3 Tuning the mORMot transport

Unlike CrossSocket (which uses THorseWorkerPool for the Horse pipeline), mORMot owns its own thread pool inside THttpServer — default 32 threads. Adjust via THorseMormotConfig and the typed entry point:

uses
  Horse, Horse.Provider.Mormot, Horse.Provider.Mormot.Config;

var
  Config: THorseMormotConfig;
begin
  Config                := THorseMormotConfig.Default;
  Config.ThreadPool     := 64;                  // bigger pool for blocking handlers
  Config.MaxBodyBytes   := 16 * 1024 * 1024;    // 16 MB
  Config.DrainTimeoutMs := 10_000;              // graceful drain window
  Config.ServerBanner   := 'MyServer/1.0';      // sent as Server: header

  THorseProviderMormot.ListenWithConfig(9000, Config);
end.

The full configuration record, http.sys swap (THttpApiServer), TLS options, and security defaults are documented in horse-provider-mormot.


10. Architecture note

Every self-hosted Provider implements the same THorseProviderAbstract base class. The Provider:

  1. Owns the listening socket.
  2. Parses an incoming request.
  3. Calls THorse.Execute(Req, Res), which runs the registered middleware chain and routes.
  4. Serialises the response back to the client.

Non-Indy Providers (CrossSocket and any future transports) use a small set of lightweight interfaces — IHorseRawRequest / IHorseRawResponse — adapted to look like TWebRequest / TWebResponse for middleware compatibility. As a result, middleware that pokes Req.RawWebRequest.Method or Res.RawWebResponse.SetCustomHeader directly (e.g. Horse.CORS) keeps working unchanged across all Providers.

If you want to build a new Provider, the hybrid-interface architecture guide in the CrossSocket repo walks through the pattern.

10.1 TCP_NODELAY on self-hosted Providers

All self-hosted Providers disable Nagle's algorithm (TCP_NODELAY) on every accepted connection:

  • Indy (Console / Daemon / VCL) sets AContext.Binding.UseNagle := False from the bridge's OnConnect.
  • CrossSocket calls TSocketAPI.SetTcpNoDelay(AConnection.Socket, True) from the server's OnConnected — in the shared transport, so every CrossSocket Application type (Console, VCL, Daemon, FPC variants) inherits it.
  • mORMot2 already enables TCP_NODELAY by default.

Why. Without it, on Linux loopback the small request/response ping-pong of an HTTP keep-alive connection collides with the kernel's ~40 ms delayed-ACK timer, pinning throughput at a flat ~44 ms/request floor (~2 270 req/s) regardless of how fast the server actually is. Disabling Nagle removes that stall. (Windows loopback doesn't trigger it, but the option is harmless-to-beneficial there too — lower latency.)

Does it affect more than small responses? It's a per-connection option, so it applies to every request, not just tiny ones — but the effect is positive or neutral for all request shapes, never harmful. It changes only when already-buffered bytes leave the socket, never what bytes or in what order; TCP remains a reliable, ordered byte stream and response content is byte-for-byte identical.

Response shapeEffect
Small responses (JSON APIs, headers-only, 204)Improved — removes the keep-alive delayed-ACK stall
Large responses (file downloads, big bodies)Neutral — Nagle only ever held back a trailing sub-MSS chunk; that final flush is now immediate
POST / uploadsNeutral — Nagle is an outbound concern; inbound body is unaffected

The classic downside of TCP_NODELAY — many tiny per-response socket writes becoming many small packets — does not apply here: Indy, CrossSocket, and Horse all buffer the full response and flush it in one (or few) writes. This matches universal HTTP-server practice (nginx, Apache, Go net/http, mORMot all set TCP_NODELAY unconditionally).

The FPC fphttpserver family and host-managed types (Apache / ISAPI / CGI / FastCGI) are not changed: fphttpserver exposes no clean per-connection hook, and host-managed deployments don't own the HTTP accept socket — the front web server's own Nagle policy applies.

10.2 Indy provider connection defaults — MaxConnections, ListenQueue & ReadTimeout

The Indy self-hosted providers (Console / Daemon / VCL) now apply safe defaults when you don't set them explicitly:

PropertyOld effective defaultNew defaultWhy
THorse.MaxConnections32 (WebBroker's Web.WebReq.TWebRequestHandler module-pool cap)1024The 32 cap returns ~60 % HTTP 500 (EWebBrokerException: "Maximum number of concurrent connections exceeded") under keep-alive + response-header middleware + concurrency ≥ ~40. Raising the module-pool ceiling makes the out-of-the-box build safe.
THorse.ListenQueue15 (Indy's IdListenQueueDefault)51115 is too small for concurrent connection bursts → dropped/refused connections. 511 mirrors nginx's backlog (the OS clamps it to net.core.somaxconn / SOMAXCONN).
THorse.ReadTimeout0 (no timeout, blocks indefinitely)0The timeout in milliseconds for socket read operations on accepted connections. Setting this (e.g. to 10000 for 10 seconds) prevents slow or hung clients from holding server sockets and threads open indefinitely. (Indy only).

Behaviour & compatibility. These are applied only when the value is left unset — if you already call THorse.MaxConnections := N, THorse.ListenQueue := N or THorse.ReadTimeout := N before Listen, your value wins, unchanged. The MaxConnections default raises only the WebBroker module-pool ceiling (the thing that produced the 500s); it does not impose an Indy TCP connection cap unless you set one. Tune both up for very high concurrency (e.g. 1000+ at c≈500).

THorse.MaxConnections := 4096;   // optional — overrides the 1024 default
THorse.ListenQueue    := 1024;   // optional — overrides the 511 default
THorse.ReadTimeout    := 10000;  // optional — sets connection read timeout to 10 seconds
THorse.Listen(9000);

Provider scope: connection settings like MaxConnections, ListenQueue, and ReadTimeout apply to the self-hosted Indy providers only (Console / Daemon / VCL).

  • Host-managed types (Apache, ISAPI, CGI, FastCGI) delegate connection lifecycle and timeouts to the host web server configuration (IIS/Apache).
  • HttpSys configures connection timeouts at the Windows kernel level (via registry or netsh).
  • CrossSocket and mORMot manage their own thread pools and network I/O; CrossSocket connection limits live in THorseCrossSocketConfig.MaxConnections, and mORMot sizes its own fixed thread pool (THorseMormotConfig.ThreadPool, default 32). The constants live in Horse.Provider.Config (DEFAULT_MAX_CONNECTIONS, DEFAULT_LISTEN_QUEUE).

How to size them — and why they're not derived from CPU count

These two values govern I/O concurrency and connection-burst absorption, not compute — so they are deliberately fixed defaults, not a function of the machine's CPU count. Size them against the variables that actually matter:

  • ListenQueue = the kernel TCP accept backlog — a burst buffer for connections waiting to be accept()ed. Size it to your peak connection-arrival burst, not your cores; a faster box drains the queue quicker and needs less backlog, not more. The OS hard-clamps it to net.core.somaxconn (Linux) / SOMAXCONN (Windows) — raise that to match if you set a high value.
  • MaxConnections = a ceiling on concurrent in-flight requests. The useful level is driven by your handlers' I/O profile: CPU-bound handlers saturate near core-count, but typical I/O-bound handlers (DB, external HTTP, files) profitably run far more concurrent requests than there are cores, because most are blocked waiting. It's a safety ceiling, so the practical limit is RAM / ulimit -n — Indy is thread-per-connection (~1 MB stack + one fd per connection), so e.g. 1024 connections ≈ up to ~1 GB of thread stacks worst-case.

Why not scale with CPU? MaxConnections is a ceiling; deriving it from cores would give small boxes a low cap and re-introduce the keep-alive 500s on exactly the machines least able to absorb them. CPU count belongs on the knobs that count multiplexing/compute threads — and those already auto-scale: CrossSocket's IO threads (CPUCount*2+1) and the CPU-bound THorseWorkerPool (4–64 threads). Connection ceilings and accept backlogs are a different category.

Rule of thumb: raise MaxConnections toward your expected peak concurrent in-flight requests (bounded by RAM/fd limits), and ListenQueue toward your expected peak accept burst (bounded by somaxconn). Leave them at the defaults until a load test says otherwise.

See also

  • Getting Started — your first Console + Indy server.
  • Middleware Ecosystem — middleware that works across all Providers.
  • Compiler Support — Delphi/FPC versions per Provider and Application type.
  • horse-provider-crosssocket — the optional CrossSocket async Provider's own documentation.
  • horse-provider-mormot — the optional mORMot2 async Provider's own documentation.
  • horse-provider-ics — the optional OverbyteICS Provider (OpenSSL 3.x/4.x TLS), Delphi-only (Windows + POSIX/Linux64/macOS).
  • HttpSys (Horse.Provider.HttpSys, built into Horse) — native Windows http.sys kernel-mode transport; no external dependency, Windows-only.
  • gRPC — Native gRPC and HTTP/2 h2c transport provider (Code-First).