Blog
All Blog Posts | Next Post | Previous Post
One Binary, No Apache: The New Sparkle Socket Server for Linux
Today
Deploying a Delphi HTTP backend on Linux has always involved a decision you didn't really want to make.
You could build an Apache module with WebBroker which means installing and configuring Apache on every server, matching module ABIs, and debugging a stack where your code is a guest in someone else's process. Or you could use the Indy-based server self-contained, but with no built-in HTTPS (you need a third-party IOHandler), no WebSockets, and modest throughput.
Meanwhile, on Windows, Sparkle servers have had a first-class, kernel-mode HTTP server for a decade.
That gap is now closed. The next version of TMS Sparkle introduces the socket server: a complete, high-performance HTTP server written entirely in Delphi, on raw sockets. Your Linux deployment becomes a single executable that you copy to the machine and run. No Apache. No nginx. No third-party libraries to install and keep updated.
A quick refresher on TMS Sparkle
TMS Sparkle is a high-performance HTTP server framework for Delphi. It's the foundation under TMS XData (REST APIs), TMS Sphinx (OAuth 2.0 / OpenID Connect) and TMS RemoteDB (remote database access over HTTP).
Sparkle separates what your server does modules, middleware, request handlers from how requests reach it. That "how" is the dispatcher, and until now on Linux your dispatcher choices were Apache or Indy. The socket server is a new dispatcher, and because it implements the same module contract as all the others, existing Sparkle, XData and RemoteDB servers run on it unchanged.
Hello, world
The server class is TSocketHttpServer, in unit Sparkle.Socket.Server. If you've ever written a Sparkle server for Windows with THttpSysServer, this will look extremely familiar:
uses
Sparkle.Socket.Server;
var
Server: TSocketHttpServer;
begin
Server := TSocketHttpServer.Create;
try
Server.AddModule(TMyServerModule.Create('http://localhost:8080/myapplication'));
Server.Start;
WriteLn('Server running. Press ENTER to stop.');
ReadLn;
Server.Stop;
finally
Server.Free;
end;
end;
That's a complete HTTP server. Compile it for Linux 64-bit, copy the binary to the machine, run it done.
Stop performs a graceful shutdown: listeners close, requests in flight are allowed to finish, and active WebSocket sessions are terminated cleanly.
A note on the host name. The socket server uses only the port of each module base URL to decide what to listen on it always binds all interfaces (IPv4 and IPv6) and routes requests to modules by path only. The host part is ignored, so
localhost,0.0.0.0or your real domain name all behave the same. That matches the Http.Sys dispatcher, which by default replaces the host with the+wildcard as well (unless you setKeepHostInUrlPrefixes). In other words, the same base URL works on both dispatchers and if you need to restrict who can reach the server, that's a job for a firewall rule or a reverse proxy.
The same applies to an XData REST API, which needs no changes at all beyond the dispatcher you host it in:
uses
XData.Server.Module,
Sparkle.Socket.Server;
Server := TSocketHttpServer.Create;
Server.AddModule(TXDataServerModule.Create(
'http://localhost:2001/tms/xdata', ConnectionPool));
Server.Start;
HTTPS that fits how Linux actually works
On Linux, certificates are PEM files on disk, usually renewed automatically by certbot. The socket server works exactly that way register modules with an https URL and point the Ssl property (TSslOptions) at your files:
Server := TSocketHttpServer.Create;
Server.Ssl.CertificateFile := '/etc/letsencrypt/live/myapp.com/fullchain.pem';
Server.Ssl.PrivateKeyFile := '/etc/letsencrypt/live/myapp.com/privkey.pem';
Server.AddModule(TMyServerModule.Create('https://myapp.com:443/myapplication'));
Server.Start;
TLS is provided by OpenSSL 3, loaded dynamically at runtime. On current distributions (Ubuntu 22.04+, Debian 12+, RHEL 9+) OpenSSL 3 is already installed, so there is nothing extra to deploy with your application. TLS 1.2 and TLS 1.3 are both supported.
Two features matter a lot in production:
Several certificates on one port (SNI). Serve multiple host names from one server, with the right certificate picked from the name the client asks for. Add TSslCertificate entries to SniCertificates; wildcards are supported:
var
Cert: TSslCertificate;
begin
Cert := TSslCertificate.Create;
Cert.HostName := '*.example.com';
Cert.CertificateFile := '/etc/myserver/example-com.pem';
Cert.PrivateKeyFile := '/etc/myserver/example-com.key';
Server.Ssl.SniCertificates.Add(Cert);
end;
Certificate renewal without downtime. Let's Encrypt certificates expire every 90 days. Restarting your server to pick up a renewal means dropping connections. Instead, call ReloadCertificates the files are re-read and applied to new connections, while listeners and established connections keep running:
// e.g. from a certbot deploy hook, or a nightly timer
Server.ReloadCertificates;
If the new files can't be loaded, an exception is raised and the current certificates stay in use a bad renewal can't take your server down.
WebSockets on Linux finally
Sparkle has supported WebSockets since 2024, but only on Windows. The socket server brings them to Linux, using the exact same API and TWebSocketMiddleware, over both ws and wss:
type
TWsEchoModule = class(THttpServerModule)
public
constructor Create(const ABaseUri: string); override;
procedure ProcessRequest(const C: THttpServerContext); override;
end;
constructor TWsEchoModule.Create(const ABaseUri: string);
begin
inherited Create(ABaseUri);
AddMiddleware(TWebSocketMiddleware.Create);
end;
procedure TWsEchoModule.ProcessRequest(const C: THttpServerContext);
var
Upgrader: IWebSocketUpgrader;
WebSocket: IWebSocket;
Msg: IWebSocketMessage;
begin
Upgrader := C.Item<IWebSocketUpgrader>;
if Upgrader = nil then
begin
C.Response.StatusCode := 400; // not a WebSocket upgrade request
Exit;
end;
WebSocket := Upgrader.Upgrade;
repeat
Msg := WebSocket.Receive;
case Msg.MessageType of
TWebSocketMessageType.Text:
WebSocket.Send('echo: ' + Msg.Text);
TWebSocketMessageType.Close:
begin
WebSocket.SendClose(WebSocketStatusCodes.NormalClosure);
Break;
end;
end;
until False;
end;
Idle WebSocket sessions are exempt from the read and keep-alive timeouts, so long-lived push connections just work.
Static files in the same binary
A REST API usually ships with something to serve: an SPA, a few images, a landing page. Use the standard TStaticModule and it all lives in one process:
uses
Sparkle.Module.Static;
Server.AddModule(TStaticModule.Create('http://localhost:8080/', '/var/www/myapp'));
Routing is by path, exactly as in every other Sparkle server so your API on /tms/xdata, a WebSocket endpoint on /ws, and static files on / coexist happily on one port.
Handlers that wait sized automatically
Here's the part that usually bites people who write their own server, and the reason a naive thread pool is not enough.
A worker thread is busy for as long as your handler runs. Handlers that return immediately need very few threads. But a handler that waits for a database query, for an API call to another service holds its worker while doing nothing at all. With a fixed pool, your throughput is capped at workers ÷ handler duration, no matter how idle the machine is. Sixteen threads and a 200 ms query means 80 requests per second, on a machine that's 95% idle.
The socket server sizes its pool on its own:
- It starts with
WorkerThreadsthreads (default: twice the processor count). - Whenever requests are waiting for a worker, it adds threads, up to
MaxWorkerThreads(default: 256). - Threads above the minimum exit after 30 seconds without work, so the pool shrinks back when the load drops.
- Requests that are handled immediately never grow the pool a CPU-bound server keeps running on its initial threads.
In most cases the defaults are simply correct and there is nothing to configure. You set these properties when you want a different shape most commonly to put a ceiling on threads that compete for a limited resource:
// A database-bound API against a pool of 50 connections:
// more workers than that would only queue up waiting for a connection.
Server.WorkerThreads := 16;
Server.MaxWorkerThreads := 50;
Built to face the internet
The socket server is designed to be exposed directly, without a reverse proxy in front of it, so the defensive parts are built in rather than left to nginx:
- Slowloris protection
HeaderReadTimeoutbounds how long a client may take to send its request head (and the TLS handshake). Slow clients never occupy a worker thread: the event loop reads request heads, and only complete requests are handed to a worker. - Overload protection
MaxConnectionscaps simultaneous connections andMaxQueuedRequestscaps how many requests may wait for a free worker. Beyond that, the server answers 503 and closes, rather than accumulating unbounded latency. - Size limits the
Limitsproperty (TSocketServerLimits) bounds the request line, individual headers, header count, total head size and body size, with the correct status code for each violation (413, 414, 431, 400).
Server.Limits.MaxBodySize := 50 * 1024 * 1024; // reject bodies over 50 MB
Server.KeepAliveTimeout := 30000;
You can still put nginx or Caddy in front if you want HTTP/2, HTTP/3, rate limiting or centralized certificates just add the forward middleware so your handlers see the original client address and scheme.
The RAD way
If you build your servers by dropping components on a data module, there's a new dispatcher component: TSparkleSocketDispatcher. Connect your TXDataServer, TSparkleStaticServer or other server components to it through their Dispatcher property, set Active to True, and you're running.
The underlying server object is available through Server for anything you need to configure from code:
uses
Sparkle.Comp.SocketDispatcher;
SparkleSocketDispatcher1.Server.Ssl.CertificateFile := '/etc/myserver/cert.pem';
SparkleSocketDispatcher1.Server.Ssl.PrivateKeyFile := '/etc/myserver/key.pem';
SparkleSocketDispatcher1.Active := True;
Deploying: a binary and a systemd unit
Deployment is about as simple as it gets. Compile for Linux 64-bit, copy the binary, and register it as a systemd service in /etc/systemd/system/myserver.service:
[Unit]
Description=My Sparkle server
After=network.target
[Service]
ExecStart=/opt/myserver/myserver
Restart=always
User=myserver
Group=myserver
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now myserver
Binding to ports below 1024 needs privileges either run as root, or grant just the one capability the binary needs:
sudo setcap cap_net_bind_service=+ep /opt/myserver/myserver
How fast is it?
Fast enough that the network is usually the interesting part, not the server.
Measured on a 4-vCPU Ubuntu 24.04 VM with wrk, 30-second runs (as always with benchmarks: your hardware, handlers and payloads will differ):
| Scenario | Socket server | Indy-based server |
|---|---|---|
| Small response, 256 connections | 127,127 req/s | 30,806 req/s |
| Chunked 64 KB responses | 67,438 req/s | 25,006 req/s |
| 100 KB responses | 59,026 req/s | 23,211 req/s |
Throughput scales monotonically with concurrency there is no dip at moderate connection counts, and no connection resets in the stress runs.
A fair-play note on that Indy column: those are tuned Indy numbers. Indy does not set TCP_NODELAY on accepted connections, so with shipping defaults Nagle's algorithm and the TCP delayed-ACK timer hold small keep-alive responses back by ~40 ms, capping each connection at roughly 24 req/s. The benchmark disables Nagle per connection so the comparison measures the servers rather than a missing socket option. The socket server sets TCP_NODELAY on every accepted socket itself, so there is nothing to remember and no such trap.
More interesting than the raw numbers is the blocking-handler behaviour, because that's what a real XData API does all day. With handlers that wait 500 ms and 256 concurrent connections, the socket server served 509 req/s against 504 for an equivalent Node.js server and 510 for Go, on the same box in the same session. That's 99100% of the arithmetic ceiling (concurrency ÷ latency): the elastic worker pool means a Delphi server waiting on a database is not at a disadvantage against the stacks people usually reach for.
And how stable is it?
Speed is the easy half. A server that is exposed to the internet has to survive the boring part days of load, misbehaving clients, and load it cannot possibly serve. The socket server was built and measured with that in mind:
- It doesn't leak. Through hours of continuous benchmarking, thread count and open file descriptors tracked concurrency exactly and returned to baseline between runs, with resident memory flat at the end of the campaign.
- No errors where it counts. Every database-shaped scenario handlers waiting 50 ms to 500 ms, at 64 and 256 concurrent connections completed with zero errors and zero dropped connections, on both Linux and Windows.
- Predictable at the limit. When load exceeds what the configured workers can serve, it answers 503 and closes rather than queueing without bound, so an overloaded server stays responsive and recovers as soon as the spike passes. That threshold is yours to size, via
MaxWorkerThreadsandMaxQueuedRequests. - Malformed and hostile input is handled by design, not by luck. The HTTP parser was fuzzed, and every limit violation maps to a defined status code rather than an unhandled exception.
- It runs the same test suite as every other dispatcher. The TMS BIZ automated tests execute against the socket server on Linux and Windows, so the module, middleware and WebSocket behaviour you rely on is verified on it not just on the Windows dispatchers.
And on Windows, too
The socket server is cross-platform: it runs on Windows 32-bit and 64-bit as well, with the same code, the same properties and the same behaviour. (Internally it uses epoll on Linux and WSAPoll on Windows, which is about the only thing that differs.)
On Windows the Http.Sys-based server remains the recommended choice for high-load production servers it's kernel-mode and integrates with the Windows certificate store. Reach for the socket server on Windows when:
- your application can't run administrative setup. Http.Sys needs
netshURL reservations and certificate bindings; the socket server binds plain sockets and needs none, so it runs under any user account. - you want identical behaviour on both platforms develop on Windows, deploy on Linux, with one configuration and no surprises.
- you prefer PEM certificates with SNI and hot reload over the Windows certificate store.
One more thing for Linux
A related fix in the same release removes another long-standing Linux annoyance: Sparkle now registers JOSE cryptography providers backed by OpenSSL 3, so JWT tokens signed with RSA (RS256/384/512) and ECDSA (ES256/384/512) work on distributions that no longer ship the legacy OpenSSL 1.x libraries Ubuntu 22.04 and later, among others. This is what lets a TMS Sphinx server using RSA-signed tokens run on a modern Linux box. The providers are registered automatically and fall back to the previous implementation when OpenSSL 3 isn't available, so existing applications need no changes.
Conclusion
The socket server makes Linux a first-class deployment target for Delphi HTTP servers:
- One self-contained binary no Apache, no nginx, no third-party libraries to install or keep updated.
- Everything a public-facing server needs HTTPS with SNI and zero-downtime certificate reload, WebSockets, static files, keep-alive, chunked encoding, and built-in protection against oversized, slow and excessive requests.
- Fast, and steady under load including the case that matters most, handlers that wait on a database.
- Nothing to rewrite existing Sparkle, XData and RemoteDB modules run on it as they are.
- Cross-platform the same server, the same configuration, on Linux and Windows.
It is now the recommended way to deploy TMS Sparkle, TMS XData and TMS RemoteDB servers on Linux.
A complete demo dynamic endpoint, static files, WebSocket echo and optional HTTPS, in a single program that builds for both platforms ships in the Sparkle distribution under demos/SocketServer.
To learn more, read the full Socket Server documentation and the what's new list, and visit the TMS Sparkle product page.
Wagner Landgraf
This blog post has not received any comments yet.
All Blog Posts | Next Post | Previous Post