LinkedIn Sourceforge

Vincent's Blog

Pleasure in the job puts perfection in the work (Aristote)

I test my new version of Fapws4 against other wsgi solutions and even against apache

Posted on 2026-03-14 22:04:00 from Vincent in FreeBSD fapws

I've made lot of modificaitons inside Fapws4 those last days/weeks. It remains a strong and very fast WSGI server offering all flexibility of Python. This is mainly than,ks to the libuv library and the picohttp parser. This blog will show how Fapws4 resist to massive requests and to compare results with other SWGI solutions. I will even compare it with apache24.


Photo by Wang Sheeran on unsplash.com

FAPWS is still in the game ;) — FreeBSD edition

It's been about two years since I ran the previous benchmark on an OpenBSD machine. At that time, Fapws4 was already holding its own against the competition. I recently set up a Lenovo ThinkPad T14s with an AMD Ryzen 7 PRO 7840U, 16 threads and 32 GB of RAM, running FreeBSD 14.3-RELEASE-p8 (GhostBSD). I couldn't resist re-running the same tests.

Introduction

In the previous post, I compared Fapws4 against Flask, FastAPI, Gunicorn and OpenBSD's built-in httpd on a Lenovo T460. The conclusion was clear: Fapws4 was at the level of httpd — a pure C web server — while serving dynamically generated Python responses.

This time I'm doing the same exercise on FreeBSD, with a more powerful machine. Same protocol, same bench script, new platform. Let's see if the gap has closed.

How to compare webservers ?

Which tool

Same as before: ApacheBenchmark (ab). On FreeBSD it comes bundled with the apache24 package. On this system the version is 2.4.x.

The two parameters I use:

-n requests Number of requests to perform -c concurrency Number of multiple requests to make at a time

How to measure it ?

Same bench.sh script as described in the previous post, unchanged. It handles waiting for netstat to settle back below 1000 open connections before each run, and repeats each scenario 3 times to check consistency.

#!/bin/sh

SERVER="http://127.0.0.1:8080"

NET_INIT=1000

echo "init netstat:$NET_INIT"
wait_netstat()
{
    NETSTAT=$(netstat -a | wc -l)
    while [ $NETSTAT -gt $NET_INIT ]
    do
         echo -ne "netstat: $NETSTAT\r"
         sleep 1
        NETSTAT=$(netstat -a | wc -l)
    done
}

bench ()
{
    for i in 1 2 3
    do
        wait_netstat
        curr_date=$(date)
        echo "$curr_date: $1 $2 $SERVER$3" >> bench.errorlog
        res=$(ab -n$1 -c$2 $SERVER$3 2>>bench.errorlog | grep Requests)
        val=$(echo $res | cut -d' ' -f4)
        echo "$1 $2 $3: $val"
    done
}

bench 1000 10 /
bench 10000 10 /
bench 50000 10 /
bench 10000 100 /
bench 50000 100 /
bench 10000 500 /
bench 50000 500 /
bench 10000 1000 /
bench 50000 1000 /

Now that we have our torture instrument, let's look at the contenders.

Which WSGI servers are available ?

Flask: installation

$ pkg install py311-flask

New packages to be INSTALLED:
    py311-asgiref: 3.11.1 [GhostBSD]
    py311-flask: 3.1.3 [GhostBSD]
    py311-itsdangerous: 2.2.0 [GhostBSD]
    py311-python-dotenv: 1.2.1 [GhostBSD]
    py311-werkzeug: 3.1.5 [GhostBSD]

$ flask --version
Python 3.11.14
Flask 3.1.3
Werkzeug 3.1.5

Same hello world script as before:

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello_world():
    return "Hello, World!"

Execution:

% export FLASK_APP=flask-hello.py
% flask run
* Serving Flask app 'flask-hello.py'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment.
* Running on http://127.0.0.1:5000

Flask: benchmark

1000 10 /: 850.81
1000 10 /: 853.47
1000 10 /: 856.62
10000 10 /: 861.17
10000 10 /: 866.81
10000 10 /: 864.29
50000 10 /: 863.64
50000 10 /: 863.08
50000 10 /: 862.96
10000 100 /: 2510.60
10000 100 /: 2618.04
10000 100 /: 2591.96
50000 100 /: 2610.22
50000 100 /: 2555.49
50000 100 /: 2558.07
10000 500 /:
10000 500 /:
10000 500 /:
50000 500 /:
50000 500 /:
50000 500 /:
10000 1000 /:
...

Flask: observations

Flask is consistent at low concurrency, hovering around 860 req/s at 10 concurrent connections. Pushing to 100 concurrent connections bumps it to roughly 2,600 req/s. Above 500 concurrent requests it collapses entirely with Test aborted after 10 failures. On the previous test with OpenBSD it held on up to 500 concurrent requests — so FreeBSD's default system limits are tighter here. We will see this wall with every server under test.

Compared to the OpenBSD results where Flask delivered ~1,100 req/s, the FreeBSD numbers are slightly lower despite the more powerful hardware — the development server is the bottleneck, not the machine.

FastAPI: installation

$ pkg install py311-fastapi

New packages to be INSTALLED:
    py311-annotated-doc: 0.0.4 [GhostBSD]
    py311-fastapi: 0.134.0 [GhostBSD]
    py311-python-multipart: 0.0.22 [GhostBSD]
    py311-starlette: 0.52.1 [GhostBSD]

Unlike on OpenBSD where I installed via pip, here FastAPI comes from the FreeBSD package manager. Unfortunately, trying to run it immediately fails:

$ fastapi run fastapi-hello.py
RuntimeError: To use the fastapi command, please install "fastapi[standard]":
    pip install "fastapi[standard]"

After investigation, the FreeBSD package does not pull in Uvicorn, which FastAPI depends on to run. Since FastAPI without Uvicorn is essentially Gunicorn, the results would mirror the next section anyway. I'll skip dedicated FastAPI numbers here.

Gunicorn: installation

$ pkg install py311-gunicorn

New packages to be INSTALLED:
    py311-gunicorn: 25.1.0 [GhostBSD]
    py311-setproctitle: 1.3.3_1 [GhostBSD]

Same WSGI application as in the previous test. Launched with 4 worker processes:

$ gunicorn -w4 hello:application
[2026-03-14 21:19:13 +0100] [20940] [INFO] Starting gunicorn 25.1.0
[2026-03-14 21:19:13 +0100] [20940] [INFO] Listening at: http://127.0.0.1:8000 (20940)
[2026-03-14 21:19:13 +0100] [20940] [INFO] Using worker: sync
[2026-03-14 21:19:13 +0100] [20941] [INFO] Booting worker with pid: 20941
[2026-03-14 21:19:13 +0100] [20942] [INFO] Booting worker with pid: 20942
[2026-03-14 21:19:13 +0100] [20943] [INFO] Booting worker with pid: 20943
[2026-03-14 21:19:13 +0100] [20944] [INFO] Booting worker with pid: 20944

Gunicorn: benchmark

1000 10 /: 47725.86
1000 10 /: 51442.98
1000 10 /: 51607.58
10000 10 /: 55587.67
10000 10 /: 55476.04
10000 10 /: 55587.05
50000 10 /: 55866.30
50000 10 /: 55307.16
50000 10 /: 55771.02
10000 100 /: 56053.50
10000 100 /: 56030.26
10000 100 /: 55674.64
50000 100 /: 56089.34
50000 100 /: 55625.83
50000 100 /: 55047.41
10000 500 /:
...

Gunicorn: observations

Impressive and very consistent: ~55,000 req/s across all tested concurrency levels, flat as a board from 10 to 100 concurrent connections. This is a massive jump compared to the OpenBSD results where it delivered ~5,200 req/s — a 10x difference, largely explained by the newer, more powerful hardware and the 4 pre-forked workers making full use of the Ryzen's cores.

Same wall at 500 concurrent requests though.

Fapws4: installation

For this test, I'm using version 0.4 of Fapws4, released on March 14, 2026. For installation details, refer to the Fapws4 guide directly.

Same hello world script as in the previous test:

from fapws4.base import Start_response, Environ

def hello(env, resp):
    return "hello world!"

PATHS = [('/', hello)]

$ fapws4-3.11 hello.py

Fapws4: benchmark

1000 10 /: 125219.13
1000 10 /: 135043.89
1000 10 /: 135483.00
10000 10 /: 147360.04
10000 10 /: 150795.45
10000 10 /: 148780.74
50000 10 /: 150717.72
50000 10 /: 150784.53
50000 10 /: 148214.02
10000 100 /: 145300.26
10000 100 /: 144562.99
10000 100 /: 146756.68
50000 100 /: 149742.74
50000 100 /: 148535.44
50000 100 /: 148549.12
10000 500 /:
...

Fapws4: observations

The first run at 1,000 requests is a bit lower, as usual — the server is warming up. After that, Fapws4 settles into a very consistent ~150,000 req/s across all concurrency levels. Completely flat, no degradation whatsoever as load increases.

That's roughly 3x Gunicorn and 170x Flask's dev server — for a dynamically generated Python response. Just let that sink in for a moment.

Apache24: benchmark

Just like I compared against OpenBSD's httpd in the previous post, let's see what FreeBSD's default web server can do. I installed apache24, kept every single default parameter, and dropped a hello.html file with the content Hello World!! (the same 13 bytes returned by the other servers) into /usr/local/www/apache24/data/:

$ service apache24 onestart

Results are:

1000 10 /hello.html: 113791.53
1000 10 /hello.html: 117274.54
1000 10 /hello.html: 120656.37
10000 10 /hello.html: 138640.49
10000 10 /hello.html: 135945.30
10000 10 /hello.html: 139219.54
50000 10 /hello.html: 138352.39
50000 10 /hello.html: 135667.17
50000 10 /hello.html: 135413.28
10000 100 /hello.html: 149788.05
10000 100 /hello.html: 151830.31
10000 100 /hello.html: 150870.52
50000 100 /hello.html: 153184.40
50000 100 /hello.html: 152589.29
50000 100 /hello.html: 147756.76
10000 500 /hello.html:
...

Apache tops out around ~152,000 req/s at 100 concurrent connections, serving a static file. Fapws4 matches it — serving a dynamic Python response. Same story as with OpenBSD's httpd in the previous test, just with bigger numbers on faster hardware.

And yes, Apache hits the same 500 concurrent connection wall as everyone else, which confirms this is a FreeBSD kernel parameter to tune — nothing to do with the servers themselves.

Conclusions

Two years later, on a different OS, with faster hardware: the conclusion is exactly the same.

Fapws4 is still one of the fastest web servers I can test. It matches a zero-config Apache24 serving static files, while running dynamic Python code. It's ~3x faster than Gunicorn with 4 workers, and the gap with Flask's dev server is simply not worth discussing.

Fapws4 doesn't have all the bells and whistles of the other WSGI servers, but what it does, it does extraordinarily fast. The credit goes to libuv, the C library at its core.

The 500 concurrent connection ceiling is clearly a FreeBSD system tuning issue — every single server hits the same wall — and worth a separate investigation.

Next test in another 2 years? Maybe on DragonFlyBSD this time ;)



0, 0
displayed: 57



What is the last letter of the word Python?