Read this in English or Português (BR).
Same Horse code, seven deployment shapes, two interchangeable async transports (CrossSocket and mORMot2). This page is the at-a-glance reference; for the rationale and longer code samples, see Providers & Application types §8 (CrossSocket) or §9 (mORMot2).
Every shape on this page follows the same four steps:
-
Define exactly one transport in Project Options → Conditional Defines:
HORSE_PROVIDER_CROSSSOCKETfor the CrossSocket Provider, orHORSE_PROVIDER_MORMOTfor the mORMot2 Provider.
Add the matching
HORSE_APPTYPE_*if you want the cross-product convenience unit (since PATCH-HORSE-2). The legacyHORSE_CROSSSOCKETalias still works for backwards compatibility; there is no legacy alias for mORMot. The two Provider defines are mutually exclusive —Horse.pasrejects the combination at compile time. -
Pick the project type for the desired shape.
-
Call
THorse.Listen(port)from the right lifecycle hook for that shape. -
Call
THorse.StopListenfrom the shutdown hook so the Provider drains active requests.
The runtime check that drives shape behaviour is IsConsole:
IsConsole = True(console binary,{$APPTYPE CONSOLE}) →Listenblocks the calling thread; the shutdown hook unblocks it viaStopListen.IsConsole = False(VCL / LCL / TService) →Listenstarts the IO threads and returns immediately; the calling thread is free for the GUI message loop or the service-control loop.
| Shape | {$APPTYPE CONSOLE} |
Project type | Listen from |
StopListen from |
|---|---|---|---|---|
| Console (Delphi) | ✔ | Console Application | begin … end. |
SetConsoleCtrlHandler |
| VCL (Delphi) | ❌ | VCL Forms Application | FormCreate |
FormClose |
| Linux daemon (Delphi) | ✔ | Console (Linux64 target) | begin … end. |
POSIX signal(SIGTERM, …) |
| Windows Service (Delphi) | ❌ | Service Application | ServiceStart (worker thread) |
ServiceStop |
| Linux daemon (FPC) | ✔ | Console (FPC) | begin … end. |
fpSignal(SIGTERM, …) |
| LCL desktop (FPC) | ❌ | Lazarus Application | FormCreate |
FormClose |
| FPC HTTPApplication | ✔ | Console (FPC) | begin … end. |
fpSignal(SIGTERM, …) |
{$APPTYPE CONSOLE}
function CtrlHandler(dwCtrlType: DWORD): BOOL; stdcall;
begin
if dwCtrlType in [CTRL_C_EVENT, CTRL_BREAK_EVENT, CTRL_CLOSE_EVENT, CTRL_SHUTDOWN_EVENT] then
begin
THorse.StopListen;
Result := True;
end
else
Result := False;
end;
begin
SetConsoleCtrlHandler(@CtrlHandler, True);
THorse.Listen(9000);
end.procedure TfrmMain.FormCreate(Sender: TObject);
begin
THorse.Listen(9000);
end;
procedure TfrmMain.FormClose(Sender: TObject; var Action: TCloseAction);
begin
THorse.StopListen;
end;{$APPTYPE CONSOLE}
{$IFDEF LINUX}
procedure HandleSignal(ASignal: Integer); cdecl;
begin
THorse.StopListen;
end;
{$ENDIF}
begin
{$IFDEF LINUX}
signal(SIGTERM, @HandleSignal);
signal(SIGINT, @HandleSignal);
{$ENDIF}
THorse.Listen(9000);
end.systemd unit (/etc/systemd/system/myhorse.service):
[Unit]
After=network.target
[Service]
Type=simple
ExecStart=/opt/myhorse/MyDaemon
Restart=on-failure
[Install]
WantedBy=multi-user.targetprocedure TMyHorseService.ServiceStart(Sender: TService; var Started: Boolean);
begin
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;
FListenerThread.WaitFor;
FreeAndNil(FListenerThread);
Stopped := True;
end;Install / uninstall via the standard SCM verbs:
MyHorseServer.exe /install
sc start MyHorseService
sc stop MyHorseService
MyHorseServer.exe /uninstallSimpler alternative without writing a TService: build the Console binary above and wrap with NSSM — nssm install MyHorseService C:\path\to\Console.exe. NSSM sends Ctrl+Break on stop, which the CtrlHandler from the Console shape catches.
{$MODE DELPHI}{$H+}
{$APPTYPE CONSOLE}
{$IFDEF UNIX}
procedure HandleSignal(ASignal: cint); cdecl;
begin
THorse.StopListen;
end;
{$ENDIF}
begin
{$IFDEF UNIX}
fpSignal(SIGTERM, @HandleSignal);
fpSignal(SIGINT, @HandleSignal);
{$ENDIF}
THorse.Listen(9000);
end.systemd unit identical to the Delphi Linux daemon above.
procedure TfrmMain.FormCreate(Sender: TObject);
begin
THorse.Listen(9000);
end;
procedure TfrmMain.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
THorse.StopListen;
end;Same code as the FPC Linux daemon — THorse.Listen owns the loop; don't call fphttpapp.Application.Run. If a library expects TFPHTTPApplication to exist, instantiate it but leave its Run alone.
The shutdown signal differs by OS and supervisor. All cases end in the same place: THorse.StopListen → the active Provider's SEC-30 active-request drain (implemented identically in horse-provider-crosssocket and horse-provider-mormot) → Listen returns → process exits cleanly.
| Supervisor | Signal sent | How your code catches it |
|---|---|---|
| Terminal (Ctrl-C) | SIGINT / CTRL_C_EVENT |
SetConsoleCtrlHandler (Windows) / signal / fpSignal (POSIX) |
| systemd | SIGTERM |
POSIX signal / fpSignal handler |
| Windows SCM | SERVICE_CONTROL_STOP |
TService.OnStop event |
| NSSM | Ctrl+Break (via console) |
SetConsoleCtrlHandler catching CTRL_BREAK_EVENT |
| VCL / LCL window close | WM_CLOSE |
TForm.OnClose event |
Docker docker stop |
SIGTERM (then SIGKILL after grace period) |
POSIX signal / fpSignal handler — give your handler < 10 s to return |
| Symptom | Cause | Fix |
|---|---|---|
| Process exits immediately after start | Console binary with no signal handler reaches end. after Listen returns |
Add the CtrlHandler / signal setup before Listen. |
| VCL form freezes on startup | {$APPTYPE CONSOLE} accidentally left in the .dpr |
Remove that directive; VCL/LCL apps must have IsConsole = False. |
| Windows service hangs in "Starting" state | ServiceStart blocks because Listen was called directly on the SCM thread |
Wrap Listen in TThread.CreateAnonymousThread (see TService snippet). |
| systemd reports "main process exited, code=killed, status=15/TERM" | Process didn't catch SIGTERM — systemd had to escalate |
Install the POSIX signal handler so the binary exits cleanly under 0. |
Address already in use after restart |
Previous process held the socket and was force-killed (no clean drain) | Always call StopListen; for Docker, set --stop-grace-period=30s. |
| In-progress requests lost on shutdown | Listen returned immediately after StopListen without waiting for the active-request counter |
SEC-30 already handles this — make sure you're on horse-provider-crosssocket >= 1.0.4 against a recent winddriver/Delphi-Cross-Socket (or the freitasjca/Delphi-Cross-Socket v1.0.3 fork), or any release of horse-provider-mormot, where SEC-30 has been built in from day one. |
~60 % HTTP 500 under load on Indy (EWebBrokerException: "Maximum number of concurrent connections exceeded"), only with response-header middleware + keep-alive + concurrency ≥ ~40 |
WebBroker's module-pool MaxConnections defaulted to 32 |
Fixed by default — the Indy providers now raise the ceiling to DEFAULT_MAX_CONNECTIONS (1024) when THorse.MaxConnections is unset. Set THorse.MaxConnections := N to go higher. (Indy only; see Providers §10.2.) |
| Connections refused / dropped under bursts on Indy | Indy's ListenQueue defaulted to 15 |
Fixed by default — Indy providers now use DEFAULT_LISTEN_QUEUE (511) when THorse.ListenQueue is unset; raise the OS somaxconn to match for very high concurrency. |
Most teams ship the same Horse code as Linux daemon in production and a Windows Service or Console binary for dev. The Conditional Defines stay the same — only the project target changes (Win64 / Linux64). Build twice, once per OS.
Shared CI config:
jobs:
build-linux:
runs-on: ubuntu-latest
steps: [...]
env:
CONFIGURATION: Release
PLATFORM: Linux64
DEFINES: HORSE_PROVIDER_CROSSSOCKET
build-windows:
runs-on: windows-latest
steps: [...]
env:
CONFIGURATION: Release
PLATFORM: Win64
DEFINES: HORSE_PROVIDER_CROSSSOCKETThe same .dpr compiles on both — only the deployment shell (systemd unit vs. SCM service registration) differs per OS.
Both HORSE_PROVIDER_CROSSSOCKET and HORSE_PROVIDER_MORMOT use OpenSSL for HTTPS — they dlopen / LoadLibrary the system shared library at startup. The transport stack itself is in your binary; OpenSSL is not statically linked by default. Plan your deployment accordingly.
Install OpenSSL via the distro package manager so libssl.so and libcrypto.so are on the loader path:
# Debian / Ubuntu (22.04+, OpenSSL 3.x)
apt install libssl3 libcrypto3
# Debian / Ubuntu (20.04, OpenSSL 1.1.x)
apt install libssl1.1
# RHEL / Rocky / Alma 9.x (OpenSSL 3.x)
dnf install openssl-libs
# Alpine
apk add openssl libcrypto3 libssl3Both providers accept either 1.1.x or 3.x — they probe at startup. If the loader can't find either, the binary still runs but SSLEnabled := True fails at Listen-time with a clear "no SSL backend available" error.
For air-gapped or minimal container deployments where you cannot rely on the distro packages:
- CrossSocket: ship the matching
libssl.so+libcrypto.soalongside your binary and declare them in the systemd unit's[Service]section:Environment="LD_LIBRARY_PATH=/opt/yourapp". - mORMot2: the
mormot2staticpackage includes a static-link variant for some platforms (mormot2static/static/x86_64-linuxfor FPC) — see horse-provider-mormot's samples/tests/README for the full Search-path setup.
Ship the OpenSSL DLLs next to your .exe (not into C:\Windows\System32 and not into a globally-PATHed folder — co-locating them avoids hijacking by other apps' bundled OpenSSL):
| OpenSSL version | DLL names (per-arch) |
|---|---|
| 1.1.x | libssl-1_1-x64.dll, libcrypto-1_1-x64.dll (Win64); drop -x64 for Win32 |
| 3.x | libssl-3-x64.dll, libcrypto-3-x64.dll (Win64); drop -x64 for Win32 |
The standard source is the official OpenSSL Windows builds or SLProWeb's installers. Pick one version and use it everywhere — code that dynamic-loads libssl-1_1.dll will not run on a host that only has libcrypto-3.dll, and the two cannot coexist within the same process.
For Windows Service deployments, the DLLs must be in the same folder as the service .exe — the SCM does not inherit the user's PATH.
| Symptom | Cause | Fix |
|---|---|---|
EOSError: failed to load libssl on Linux |
No OpenSSL package installed, or runtime is stripped to a minimal container with only libc |
Install libssl3 / libssl1.1 (Linux) or copy the DLLs next to the binary (Windows). |
| HTTPS works in dev, fails in prod with "wrong version number" | Dev box has OpenSSL 3.x; prod box has 1.1.x (or vice versa) — TLS negotiation features differ | Pin to one version family across environments. If you must support both, ship the DLLs (Windows) or use the mormot2static static-link variant (mORMot2, Linux). |
| Random SIGSEGV during TLS handshake on Linux | Two copies of libcrypto loaded at once (system 3.x + a different bundled 1.1.x in LD_LIBRARY_PATH) |
Ensure only one OpenSSL ABI is reachable. |
- Providers & Application types — the full architectural model, including §8 (CrossSocket) and §9 (mORMot2) with annotated code for every shape.
- Getting Started — your first Horse server before deciding on a deployment shape.
- Compiler Support — Delphi / FPC version requirements and the platform matrix.
horse-provider-crosssocket— config (TLS, body-size limits, IO thread count) for the CrossSocket Provider.horse-provider-mormot— config (thread pool, max body bytes, drain timeout, server banner) for the mORMot2 Provider.