I was thinking about a baseline for comparing the web server I was testing to. So I visited Static Web Server’s website and noticed the updated website design, and focus on a new v3 beta version.
This interested me, so in this blog post, I’m going to compare two web servers:
- Static Web Server (SWS) v3.0.0-beta.1
- Ferron 3.0.0-beta.11 (wait… is this post in the Ferron blog?)
What are they?
Static Web Server v3 is according to its documentation “a fast and efficient, open-source web server for serving static files and web assets”. SWS is designed specifically for fast static file serving and is very lightweight (~4MB binary).
Ferron 3 in the other hand is according to its documentation “a configurable web server for static sites, reverse proxying, TLS, and more”, and it’s designed for various use cases, including reverse proxying too.
For this blog post though, I’m going to focus on static file serving features of both servers.
Configuration
Static Web Server v3 can be configured either via command-line arguments, environment variables, or a TOML configuration file that looks like this:
[general]
host = "::"
port = 80
root = "./public"Ferron 3 in the other hand can be configured via a configuration file that looks like this:
*:80 {
root "./public"
}They are both easy to configure (Ferron’s configuration format, and Static Web Server’s -d wwwroot CLI arguments). Though Ferron 3 supports automatic TLS certificate management, while Static Web Server v3 doesn’t seem to support it. Though certbot can be used (just like with any web server that supports HTTPS), and it can make sense given SWS’s lightweight footprint.
Observability
From https://ferron.sh/blog/web-infrastructure-observability:
Yes, even static file serving benefits from observability, although much of it would be delegated to CDNs…
With observability, operators know the 404 error rate was increasing after some time, and this can be used to determine this is due to a failed deployment that ended up breaking the frontend. Also, they would know what path caused the 404 errors.
When it comes to observability, both Ferron and SWS offers options for observing the web server.
SWS v3 supports basic RED metrics via Prometheus (such as sws_http_requests_total or sws_http_response_bytes_total), which can be enabled like this:
[general]
metrics = trueThe metrics can then be used with Grafana (SWS even provides an example dashboard) to monitor the infrastructure and set up alerting rules for monitoring.
SWS can be also configured to emit structured newline-delimited JSON logs to either stderr or a log file, which then can be used with various log shippers (like Fluent Bit, Vector or Filebeat).
Ferron 3 in the other hand offers full OpenTelemetry, Prometheus and log files for observability (it’s literally designed for less debugging friction). For example, OpenTelemetry can be enabled in Ferron 3 like this:
example.com {
observability {
provider otlp
logs https://collector:4318/v1/logs
metrics https://collector:4318/v1/metrics
traces https://collector:4317/v1/traces
service_name "my-service"
}
}Even with CLF-style log files, they have trace IDs by default for improved correlation. Ferron 3 supports metrics, logs, traces, and correlation between them (trace IDs, even exemplars).
I would say Ferron 3 offers far more comprehensive observability than SWS v3, although for basic static file serving, log files and maybe Prometheus metrics can be good enough (which SWS supports too).
Performance
Now comes the exciting part.
For the performance testing, I used a laptop with Intel Core Ultra 7 258V for both client and server, which ran Linux 7.1 (Omarchy, based on Arch Linux). I configured both servers to use HTTP/2 (if applicable), HTTPS, custom webroot and custom port. Everything else was left default.
Below are the results:
❯ # Ferron
❯ wrk -c 1000 -d 10 -t 8 https://localhost:8443 # ~3.4KB static file, HTTPS + HTTP/1.1
Running 10s test @ https://localhost:8443
8 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 43.40ms 193.61ms 2.00s 95.15%
Req/Sec 38.47k 15.08k 89.02k 69.44%
3048502 requests in 10.09s, 10.65GB read
Socket errors: connect 0, read 0, write 0, timeout 44
Requests/sec: 301998.52
Transfer/sec: 1.05GB
❯ h2load --duration 10s -c 250 -t 8 -m 8 https://localhost:8443/ # ~3.4KB static file, HTTPS + HTTP/2
[skipped for brevity]
finished in 10.03s, 480886.40 req/s, 1.53GB/s
requests: 4808864 total, 4810864 started, 4808864 done, 4808864 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 4808864 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 15.33GB (16456046435) total, 45.96MB (48191967) headers (space savings 96.78%), 15.20GB (16321284416) data
min max median p95 p99 mean sd +/- sd
request : 80us 652.75ms 3.44ms 9.29ms 12.24ms 4.10ms 7.17ms 98.45%
connect : 12.89ms 654.09ms 102.82ms 435.68ms 607.19ms 144.29ms 130.75ms 85.60%
TTFB : 71.20ms 701.45ms 259.13ms 692.94ms 701.40ms 308.41ms 207.50ms 63.60%
req/s : 815.01 4866.68 1679.05 4423.14 4752.07 1923.31 1000.25 63.60%
❯ wrk -c 1000 -d 10 -t 8 http://localhost:8080 # ~3.4KB static file, HTTP/1.1
Running 10s test @ http://localhost:8080
8 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 2.82ms 2.51ms 26.43ms 68.52%
Req/Sec 48.92k 8.13k 79.14k 65.25%
3910772 requests in 10.09s, 13.53GB read
Requests/sec: 387542.58
Transfer/sec: 1.34GB
❯ wrk -c 1000 -d 10 -t 8 http://localhost:8080/favicon.ico # ~111KB static file, HTTP/1.1
Running 10s test @ http://localhost:8080/favicon.ico
8 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 6.07ms 6.10ms 46.60ms 85.38%
Req/Sec 24.47k 5.87k 45.58k 69.80%
1955354 requests in 10.10s, 203.60GB read
Requests/sec: 193626.26
Transfer/sec: 20.16GB❯ # SWS
❯ wrk -c 1000 -d 10 -t 8 https://localhost:8443 # ~3.4KB static file, HTTPS + HTTP/1.1
Running 10s test @ https://localhost:8443
8 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 6.41ms 29.06ms 566.11ms 98.02%
Req/Sec 37.04k 9.94k 50.96k 85.86%
2825186 requests in 10.08s, 10.23GB read
Requests/sec: 280219.57
Transfer/sec: 1.01GB
❯ h2load --duration 10s -c 250 -t 8 -m 8 https://localhost:8443/ # ~3.4KB static file, HTTPS + HTTP/2
[skipped for brevity]
finished in 10.03s, 263606.00 req/s, 869.84MB/s
requests: 2636060 total, 2638060 started, 2636060 done, 2636060 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 2636060 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 8.49GB (9120895309) total, 98.15MB (102920299) headers (space savings 90.98%), 8.33GB (8946787640) data
min max median p95 p99 mean sd +/- sd
request : 21us 132.19ms 6.24ms 16.19ms 20.99ms 7.10ms 4.90ms 68.99%
connect : 4.40ms 1.18s 87.27ms 1.13s 1.17s 342.11ms 460.81ms 73.20%
TTFB : 15.24ms 1.19s 96.55ms 1.14s 1.17s 351.15ms 458.38ms 73.20%
req/s : 926.23 1141.65 1082.77 1113.14 1127.57 1054.16 60.98 69.20%
❯ wrk -c 1000 -d 10 -t 8 http://localhost:8080 # ~3.4KB static file, HTTP/1.1
Running 10s test @ http://localhost:8080
8 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 2.45ms 6.47ms 212.78ms 99.66%
Req/Sec 53.65k 6.03k 77.52k 82.12%
4277226 requests in 10.04s, 15.49GB read
Requests/sec: 425884.86
Transfer/sec: 1.54GB
❯ wrk -c 1000 -d 10 -t 8 http://localhost:8080/favicon.ico # ~111KB static file, HTTP/1.1
Running 10s test @ http://localhost:8080/favicon.ico
8 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 8.96ms 16.18ms 442.51ms 98.95%
Req/Sec 14.66k 1.37k 20.91k 72.47%
1173433 requests in 10.10s, 122.37GB read
Requests/sec: 116200.25
Transfer/sec: 12.12GBBoth Ferron 3 and Static Web Server v3 show good performance, although in my scenarios, Ferron 3 had higher throughput (although generally higher latency too) than Static Web Server (well, except plaintext HTTP scenario for small file, while SWS had higher throughput, due to sendfile in Ferron 3 not having that much advantage here).
Ferron 3 went through lots of optimizations and is also using sendfile optimization for plaintext HTTP (via zincio + zincio-http), making it great for high-performance needs. SWS in the other hand doesn’t use sendfile (it uses regular tokio + hyper stack), but it uses “direct” file I/O and in-memory caching for performance optimization. Still, SWS has good performance too.
Community
Both SWS and Ferron have small yet existing communities. static-web-server/static-web-server repository on GitHub has ~2.3K stars, while ferronweb/ferron one has ~2.1K stars.
But joseluisq/static-web-server Docker Hub image has far more downloads (5M+) than ferronserver/ferron image (10K+).
Also, SWS repository on GitHub seems to be created in 2019, while Ferron one was created in 2025. This might mean SWS has accumulated battle-testing for longer than Ferron. Although lack of battle-testing is an universal problem affecting new software projects…
Conclusion
Both SWS and Ferron 3 are suitable for static file serving, with both of them supporting core static file serving features, as well as metrics and logs.
Ferron 3 has more comprehensive observability and higher static file serving throughput. Choose Ferron 3 if you value debuggability (debugging is what many sysadmins struggle with) and high performance.
SWS in the other hand is more established and has smaller footprint (~4MB binary, versus 20MB+ binary of Ferron 3). That smaller footprint would help a lot when serving static files from within a Docker container (lower memory usage, lower disk space usage, and so on). Choose SWS v3 if you’re building Docker image for serving static files (for example in microservices architecture, although many would use CDNs anyway) and value small footprint, or if you value using established software a lot.