Lighttpd on FreeBSD: Lightweight Web Server Review
Lighttpd (pronounced "lighty") is a web server built for speed and minimal resource consumption. It was created in 2003 by Jan Kneschke as a proof of concept for handling the C10K problem -- serving 10,000 concurrent connections on a single server. Over two decades later, Lighttpd remains one of the leanest web servers available, with a memory footprint that makes it viable on hardware where NGINX or Apache would be wasteful. On FreeBSD, Lighttpd uses kqueue for event-driven I/O, integrates with the rc.d service framework, and is available as both a binary package and a port. This review covers Lighttpd's architecture, FreeBSD installation and configuration, FastCGI integration, static file performance, and where it fits relative to NGINX.
What Lighttpd Does
Lighttpd is an HTTP/1.1 web server with a focus on static content serving, FastCGI backend communication, and reverse proxying. It is single-threaded with an event-driven architecture, similar to NGINX but predating it. Lighttpd does not spawn worker processes by default -- a single process handles all connections using non-blocking I/O.
Core capabilities:
- Static file serving -- efficient sendfile() support on FreeBSD, automatic MIME type detection, range requests, and conditional requests (If-Modified-Since, ETag).
- FastCGI -- native FastCGI support for connecting to PHP-FPM, Python WSGI servers, Ruby, and other backends. Lighttpd was one of the first web servers to treat FastCGI as a primary interface rather than an afterthought.
- URL rewriting -- mod_rewrite provides regex-based URL manipulation for clean URLs, redirects, and routing.
- TLS -- native TLS support via OpenSSL or mbedTLS. SNI (Server Name Indication) for virtual hosting with per-domain certificates.
- Virtual hosting -- name-based and IP-based virtual hosts with per-host document roots and configurations.
- Compression -- mod_deflate for gzip and brotli compression of responses.
- WebDAV -- mod_webdav for file management via the WebDAV protocol.
- Authentication -- mod_auth supports basic and digest authentication with flat files, LDAP, or database backends.
What Lighttpd does not do: it does not support HTTP/2 natively in the mainline release (HTTP/2 support is available in recent versions via mod_h2), it does not have a built-in reverse proxy as capable as NGINX's, and it does not support dynamic configuration reloading without a restart (graceful restart is supported).
Installation on FreeBSD
Binary Package
shpkg install lighttpd
This installs the Lighttpd binary at /usr/local/sbin/lighttpd, the configuration at /usr/local/etc/lighttpd/lighttpd.conf, and the rc.d script at /usr/local/etc/rc.d/lighttpd.
Enable Lighttpd:
shsysrc lighttpd_enable="YES"
Ports Installation
For custom build options (specific TLS library, additional modules):
shcd /usr/ports/www/lighttpd make config make install clean
The ports build lets you select between OpenSSL and mbedTLS, enable or disable specific modules, and compile with debugging symbols if needed.
Verify Installation
shlighttpd -v lighttpd -t -f /usr/local/etc/lighttpd/lighttpd.conf
The -t flag validates the configuration file without starting the server. Always run this before starting or restarting Lighttpd in production.
Configuration
Lighttpd uses a single configuration file with an include mechanism. The syntax is unique -- not NGINX-style blocks, not Apache-style directives, but a flat key-value format with conditional blocks.
Basic Configuration
The default configuration file at /usr/local/etc/lighttpd/lighttpd.conf:
shserver.document-root = "/usr/local/www/data/" server.port = 80 server.bind = "0.0.0.0" server.username = "www" server.groupname = "www" server.pid-file = "/var/run/lighttpd.pid" server.errorlog = "/var/log/lighttpd/error.log" accesslog.filename = "/var/log/lighttpd/access.log" server.modules = ( "mod_access", "mod_accesslog", "mod_deflate", "mod_redirect", "mod_rewrite" ) mimetype.assign = ( ".html" => "text/html", ".css" => "text/css", ".js" => "application/javascript", ".json" => "application/json", ".png" => "image/png", ".jpg" => "image/jpeg", ".gif" => "image/gif", ".svg" => "image/svg+xml", ".ico" => "image/x-icon", ".txt" => "text/plain" ) index-file.names = ("index.html", "index.htm") url.access-deny = ("~", ".inc")
Create the log directory:
shmkdir -p /var/log/lighttpd chown www:www /var/log/lighttpd
Virtual Hosting
Lighttpd supports virtual hosts through conditional configuration blocks:
sh$HTTP["host"] == "example.com" { server.document-root = "/usr/local/www/example.com/" accesslog.filename = "/var/log/lighttpd/example.com-access.log" } $HTTP["host"] == "api.example.com" { server.document-root = "/usr/local/www/api/" }
Pattern matching with regex:
sh$HTTP["host"] =~ "^(www\.)?example\.com$" { server.document-root = "/usr/local/www/example.com/" }
TLS Configuration
Enable HTTPS with a Let's Encrypt certificate:
shserver.modules += ("mod_openssl") $SERVER["socket"] == ":443" { ssl.engine = "enable" ssl.pemfile = "/usr/local/etc/letsencrypt/live/example.com/combined.pem" ssl.privkey = "/usr/local/etc/letsencrypt/live/example.com/privkey.pem" ssl.openssl.ssl-conf-cmd = ("MinProtocol" => "TLSv1.2") ssl.openssl.ssl-conf-cmd += ("CipherString" => "HIGH:!aNULL:!MD5") }
Note: Lighttpd expects the certificate and chain in a single PEM file. Combine them if needed:
shcat /usr/local/etc/letsencrypt/live/example.com/fullchain.pem \ /usr/local/etc/letsencrypt/live/example.com/privkey.pem \ > /usr/local/etc/letsencrypt/live/example.com/combined.pem
Compression
Enable response compression to reduce bandwidth:
shdeflate.mimetypes = ( "text/html", "text/css", "text/javascript", "application/javascript", "application/json", "text/xml", "application/xml", "image/svg+xml" ) deflate.allowed-encodings = ("br", "gzip", "deflate") deflate.cache-dir = "/var/cache/lighttpd/compress/"
Create the cache directory:
shmkdir -p /var/cache/lighttpd/compress chown www:www /var/cache/lighttpd/compress
FastCGI Integration
FastCGI is where Lighttpd shines for dynamic content. The integration is cleaner and simpler than Apache's mod_php or even NGINX's FastCGI pass configuration.
PHP-FPM
Install PHP-FPM:
shpkg install php83 php83-extensions sysrc php_fpm_enable="YES" service php-fpm start
Configure Lighttpd to forward PHP requests to PHP-FPM:
shserver.modules += ("mod_fastcgi") fastcgi.server = ( ".php" => (( "socket" => "/var/run/php-fpm.sock", "broken-scriptfilename" => "enable" )) )
This sends all .php requests to the PHP-FPM socket. The broken-scriptfilename option fixes PATH_INFO handling for frameworks that rely on it.
Python via FCGI
For Python applications using flup or similar FCGI adapters:
shfastcgi.server = ( "/app" => (( "socket" => "/var/run/python-app.sock", "check-local" => "disable" )) )
The check-local disable is important -- it tells Lighttpd not to check for a local file before forwarding the request, which is necessary for application servers that handle all routing internally.
Spawning FastCGI Processes
Lighttpd can spawn and manage FastCGI processes directly:
shfastcgi.server = ( ".php" => (( "bin-path" => "/usr/local/bin/php-cgi", "socket" => "/tmp/php-fastcgi.socket", "max-procs" => 4, "bin-environment" => ( "PHP_FCGI_CHILDREN" => "4", "PHP_FCGI_MAX_REQUESTS" => "1000" ) )) )
This approach is simpler than running a separate PHP-FPM service but gives you less control over process management. For production, using PHP-FPM as an external service is recommended.
Static File Performance
Lighttpd's static file serving performance on FreeBSD is excellent. It uses sendfile() for zero-copy file transfer from disk to network socket, avoiding unnecessary copies through userspace.
Enable sendfile explicitly (usually enabled by default on FreeBSD):
shserver.network-backend = "sendfile"
For high-traffic static file serving, tune the connection limits:
shserver.max-connections = 4096 server.max-fds = 8192
Ensure the system allows enough file descriptors:
shsysctl kern.maxfiles=65536 sysctl kern.maxfilesperproc=32768
Caching Headers
Configure cache headers for static assets:
sh$HTTP["url"] =~ "\.(css|js|png|jpg|gif|ico|svg|woff2)$" { expire.url = ("" => "access plus 30 days") }
This requires mod_expire to be loaded in the modules list.
ETag and Last-Modified
Lighttpd generates ETags and Last-Modified headers automatically for static files. Conditional requests (If-None-Match, If-Modified-Since) are handled correctly, returning 304 Not Modified when appropriate. This reduces bandwidth for returning visitors without any configuration.
Resource Footprint
This is Lighttpd's primary selling point. A running Lighttpd process with the default configuration uses roughly 2-5 MB of resident memory. Under load with thousands of connections, memory usage scales linearly but remains far below NGINX or Apache for equivalent workloads.
Typical memory usage on FreeBSD:
- Idle: 2-3 MB RSS
- 1,000 concurrent connections: 10-15 MB RSS
- 10,000 concurrent connections: 40-60 MB RSS
Compare this with NGINX (4-8 MB idle, 20-40 MB under load) or Apache with prefork MPM (50-200 MB with a handful of workers). On constrained systems -- embedded devices, small VPS instances, Raspberry Pi running FreeBSD -- this difference matters.
Check Lighttpd memory usage:
shps aux | grep lighttpd top -p $(cat /var/run/lighttpd.pid)
Lighttpd vs NGINX for Static Sites
The question most administrators ask is whether Lighttpd is worth considering when NGINX exists. The answer depends on what you value.
Choose Lighttpd when:
- Memory is genuinely constrained (under 256 MB available for the web server).
- You need a simple, reliable static file server with minimal configuration.
- Your deployment is static-only or static + FastCGI with straightforward routing.
- You want a single-process, single-threaded architecture for simplicity.
- You are running on embedded FreeBSD systems or very small VPS instances.
Choose NGINX when:
- You need HTTP/2 or HTTP/3 support with full feature parity.
- You need advanced reverse proxy features (load balancing, health checks, circuit breakers).
- You need a large ecosystem of third-party modules.
- You want Lua scripting via OpenResty or njs for request processing.
- Your team already has NGINX operational expertise.
Performance comparison:
For pure static file serving, Lighttpd and NGINX perform within 5-10% of each other on FreeBSD with kqueue. Both saturate gigabit networks easily. The difference is not in throughput but in features and memory footprint.
Service Management
Start and manage Lighttpd:
shservice lighttpd start service lighttpd stop service lighttpd restart
Graceful restart (finishes existing connections before restarting):
shservice lighttpd gracefulrestart
Test configuration before restart:
shlighttpd -t -f /usr/local/etc/lighttpd/lighttpd.conf
Check running status:
shservice lighttpd status sockstat -4 -l | grep lighttpd
Limitations
- HTTP/2 support -- available via mod_h2 in recent versions but not as mature as NGINX's implementation.
- No HTTP/3 -- QUIC/HTTP/3 is not supported.
- Single-threaded -- the single-process architecture limits throughput on multi-core systems for CPU-bound workloads. For I/O-bound static serving, this is not a practical limitation.
- Smaller community -- fewer tutorials, fewer third-party modules, and less Stack Overflow coverage compared to NGINX.
- Configuration syntax -- the conditional block syntax is functional but less intuitive than NGINX's location blocks for complex routing.
- No dynamic reload -- configuration changes require a restart (graceful restart is supported, but there is a brief interruption).
Verdict
Lighttpd earns its place in the FreeBSD ecosystem as the web server for administrators who value simplicity and efficiency above all else. It does one thing extremely well: serve static files and forward dynamic requests to FastCGI backends with minimal resource consumption. On FreeBSD, the kqueue integration is solid, the pkg installation is straightforward, and the rc.d service management works as expected.
For static sites, documentation hosting, API gateways fronting FastCGI backends, and any scenario where you want a web server that barely registers in top, Lighttpd is an excellent choice. It will not replace NGINX for complex reverse proxy setups or high-feature-count deployments, but it was never designed to. If you need a lightweight, reliable web server on FreeBSD and nothing more, Lighttpd delivers exactly that.
Frequently Asked Questions
Does Lighttpd support HTTP/2 on FreeBSD?
Yes, through mod_h2 available in recent versions (1.4.56+). Load the module in your configuration with server.modules += ("mod_h2"). HTTP/2 requires TLS to be configured. The implementation is functional but less tested than NGINX's HTTP/2 support.
Can Lighttpd run in a FreeBSD jail?
Yes. Lighttpd runs in jails without modification. Ensure the jail has the necessary network access and that the document root, log directory, and PID file paths are accessible within the jail's filesystem.
How does Lighttpd compare to Caddy?
Caddy offers automatic HTTPS, HTTP/2, and HTTP/3 out of the box but uses significantly more memory (40 MB+ binary, Go runtime overhead). Lighttpd uses 2-5 MB. If automatic certificate management and modern protocol support matter, choose Caddy. If minimal resource usage is the priority, choose Lighttpd.
Can I use Lighttpd as a reverse proxy?
Yes, via mod_proxy. It supports HTTP backend proxying with basic load balancing. However, NGINX and Caddy offer significantly more capable reverse proxy features. If reverse proxying is your primary use case, Lighttpd is not the best choice.
Does Lighttpd work with Let's Encrypt on FreeBSD?
Yes. Use certbot or acme.sh to obtain certificates and configure them in Lighttpd's TLS settings. Lighttpd does not have built-in ACME support like Caddy, so you need an external tool and a cron job or periodic task for renewal. After renewal, restart Lighttpd to load the new certificate.
What is the maximum number of connections Lighttpd can handle on FreeBSD?
With kqueue, Lighttpd can handle tens of thousands of concurrent connections on FreeBSD. The practical limit depends on file descriptor limits (kern.maxfiles, kern.maxfilesperproc), available memory, and the server.max-connections setting. For most deployments, 4,096 to 16,384 concurrent connections is achievable on modest hardware.