se7enxweb / qbix-webserver
Pure-PHP Web Server. No NGinX or PHP-FPM needed. Handle 10x more traffic instead!
Package info
github.com/se7enxweb/qbix-webserver
Language:JavaScript
Type:project
pkg:composer/se7enxweb/qbix-webserver
Requires
- php: >=8.1
- ext-sockets: *
Requires (Dev)
- revolt/event-loop: ^1.0
Suggests
- ext-openssl: Required for HTTPS/TLS support
- ext-pcntl: Required for graceful shutdown (SIGTERM/SIGINT) and --workers=N
- amphp/http-server: Enables HTTP/2 support with multiplexed streams and HPACK compression
- revolt/event-loop: Enables Revolt event loop driver (epoll/kqueue, better performance under high concurrency)
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v1.4.0
- v1.3.0
- v1.2.0
- v1.1.0
- v1.0.0
- v0.0.4.14
- v0.0.4.13
- v0.0.4.12
- v0.0.4.11
- v0.0.4.10
- v0.0.4.9
- v0.0.4.8
- v0.0.4.7
- v0.0.4.6
- v0.0.4.5
- v0.0.4.4
- v0.0.4.3
- v0.0.4.2
- v0.0.4.1
- v0.0.4.0
- v0.0.3.1
- v0.0.3.0
- v0.0.2.9
- v0.0.2.8
- v0.0.2.7
- v0.0.2.6
- v0.0.2.5
- v0.0.2.4
- v0.0.2.3
- v0.0.2.2
- v0.0.2.1
- v0.0.2
- v0.0.1
- dev-maintain
- dev-fix/cache-recompresses-every-hit
- dev-pr/cache-recompresses-every-hit
- dev-fix/cache-body-inside-json
- dev-pr/cache-body-inside-json
- dev-fix/tls-listener-leaves-nagle-on
- dev-pr/tls-listener-leaves-nagle-on
- dev-feat/load-benchmark
- dev-pr/load-benchmark
- dev-pr/compat-misses-nullsafe-calls
- dev-pr/path-safety-tests
- dev-feat/path-safety-tests
- dev-fix/compat-misses-nullsafe-calls
- dev-pr/hpack-reads-past-the-buffer
- dev-pr/http2-resource-limits
- dev-pr/unit-test-suite
- dev-feat/unit-test-suite
- dev-fix/hpack-reads-past-the-buffer
- dev-fix/http2-resource-limits
- dev-pr/compat-misses-phar-includes
- dev-fix/compat-misses-phar-includes
- dev-pr/http2-truncated-responses
- dev-pr/cache-refresh-header
- dev-pr/http2-drops-cookies
- dev-pr/server-name-carries-the-port
- dev-fix/http2-drops-cookies
- dev-fix/server-name-carries-the-port
- dev-feat/cache-refresh-header
- dev-fix/http2-truncated-responses
- dev-fix/tls-handshake-retries-promptly
- dev-fix/tls-sessions-can-be-resumed
- dev-fix/tls-handshake-does-not-wait-for-a-timer
- dev-feat/http2
- dev-feat/configurable-cache-lifetime-for-static-files
- dev-fix/cache-the-files-that-need-no-transform
- dev-backup/maintain-before-exponential-rename
- dev-fix/uploads-pass-the-check-that-accepts-them
- dev-fix/carry-binary-request-bodies-to-the-worker
- dev-fix/do-not-prompt-when-nobody-can-answer
- dev-fix/size-the-worker-pool-from-measured-cost
- dev-fix/tell-the-worker-the-connection-is-tls
- dev-fix/raw-set-cookie-survives-the-shim
- dev-fix/file-wrapper-does-not-raise-for-missing-paths
- dev-fix/location-header-means-redirect
- dev-fix/exit-ends-the-request-not-the-worker
- dev-fix/headers-write-every-byte
- dev-fix/no-fork-for-tls-connections
- dev-fix/guard-closed-stream-handles
- dev-fix/event-loop-survives-dead-connections
- dev-fix/keep-globals-setting
This package is auto-updated.
Last update: 2026-09-23 06:46:19 UTC
README
Run your existing PHP codebase 10–100× faster than nginx + php-fpm
A pure PHP web server. No nginx, no Apache, no php-fpm. One process serves static files, PHP scripts, WebSocket connections, and a live dashboard.
The problem with php-fpm
Whether opcache is enabled or not, the vast majority of production PHP code is I/O-bound. Workers wait for the database, the filesystem, an API call, a cache server. During that wait, the worker is doing nothing — but it's still holding 30–60MB of RAM. That's the bottleneck. On a 4GB server, php-fpm gets maybe 80 workers. Each one blocks on a 200ms query, so you get ~400 req/s. That's the ceiling.
The problem with Swoole, RoadRunner, and FrankenPHP
They try to solve this by making PHP evented, like Node.js. Swoole's coroutines can multiplex I/O within a single worker — but only if you rewrite your code to use Swoole\Coroutine\MySQL, Swoole\Coroutine\Http\Client, and so on. Every PDO::query(), every file_get_contents(), every curl_exec() in every WordPress plugin, Laravel package, and Drupal module uses blocking I/O. It doesn't yield. Swoole can't help with code that doesn't cooperate. RoadRunner and FrankenPHP don't even try coroutines — they use the same worker-count-limited model as fpm.
How Qbix solves it
Instead of making each worker do more, Qbix runs more workers. The server loads your entire framework into a parent process, then calls pcntl_fork(). The kernel marks every page copy-on-write. Each worker shares the parent's loaded classes and only pays for pages it actually writes to during the request. A WordPress-like request dirties 30 pages = 120KB. So the same 4GB that gives fpm 80 workers gives Qbix thousands.
Your code runs unmodified, in two modes:
Persistent workers (default) — workers stay alive across requests. Between each request, a Reflection-based snapshot restores all static properties in 0.03ms. 28 PHP functions (header(), session_start(), ini_set(), set_error_handler(), etc.) are shimmed via source transformation so they reset correctly. This is how you get 2,294 req/s on CPU-bound work and 1,060 req/s under I/O.
Fork-per-request — if persistent mode doesn't work for your code (functions with internal static variables, plugins that register global state in ways the shim can't track), set forkPerRequest: true. Each request gets a fresh fork. It's slower than persistent mode, but each forked worker still costs only 120KB instead of 50MB, so you can run 100× more of them than fpm on the same hardware. That's the whole point — blocking I/O doesn't matter when you have enough workers, and COW makes "enough workers" nearly free.
What it replaces
| nginx + php-fpm | Qbix Server | |
|---|---|---|
| 💾 Memory per worker | 30–60MB (duplicated) | ~200KB (COW, measured) |
| 👥 Concurrent PHP (1GB) | ~24 workers | ~5,000 (typical) |
| 🔒 Isolation | Statics leak between requests | Snapshot reset — no leaks |
| 🚀 Throughput (CPU-bound) | ~400 req/s (Swoole 4w) | 2,294 req/s (100w) |
| 🚀 Throughput (I/O, same RAM) | 78 req/s (fpm/Swoole 4w) | 1,060 req/s (100w) |
| 🌐 WebSocket | Needs a separate server | Built in |
| 🧩 Cache invalidation | Whole-page only | X-Cache-Tree — per-component |
| ⚙️ Setup | nginx + fpm pools + sockets | php qbixserver.php |
See BENCHMARKS.md for full methodology and reset.md for what gets restored between requests.
What a "real-time PHP app" used to require
nginx for reverse proxy and static files. php-fpm to run PHP. Node.js for a Socket.IO server. Redis for pub/sub between fpm and Node. supervisor to keep it all running. Docker to make it deployable. Six processes, three languages, two runtimes.
Qbix Server replaces all six with one process. HTTP, WebSocket (with Socket.IO protocol), SSE, sessions, uploads, static files, .htaccess — same port, same file. No Redis, no Node, no pub/sub glue. Download a 4.5MB binary, run it, done. Pure PHP.
You can also package your entire app — code, assets, SQLite database — into that binary and distribute it as a single file. Double-click on Windows, ./myapp --open on Mac or Linux, the browser opens and the app is there. No PHP to install, no web server to configure, no database to set up. 5 MB, not 200 — because we open the browser that's already there instead of shipping Chromium like Electron does. How it works →
Documentation
| Topic | What it covers | |
|---|---|---|
| 🏎️ | Why Not php-fpm? | COW memory model, comparison with Swoole and FrankenPHP |
| 🔒 | Server Headers | Cache-Control, X-Cache-Tree, X-Accel-Redirect, ETag |
| 🌐 | HTTP | Fork-per-request mode, request lifecycle |
| 🔌 | WebSocket & Rooms | Process per connection, rooms, Socket.IO, SSE, chat example |
| 🛤️ | Routing | Clean URLs, .htaccess, DirectoryIndex |
| 📂 | PHP Framework | The micro-framework: handlers, events, Q classes |
| ⚙️ | Configuration | JSON config, CLI options, presets |
| 📦 | Running & Building | Source, phar, binary. Building static binaries. Requirements |
| 📀 | Binaries & Signing | Pack apps, manage like zip, ECDSA M-of-N signing, Rekor, platform signing |
| 🏗️ | Architecture | Persistent workers, COW, execution model, mental model, benchmarks |
| 📊 | Dashboard & Panel | Live stats, control panel tabs |
| 🚀 | Deploy & Federation | Rsync deploy, cluster replication, inter-server trust |
| 🔍 | API Discovery | OpenAPI, MCP, qbix.json, HTTP/2 |
| 🧩 | Compatibility | SAPI emulation, 28 shimmed functions, class ownership, tests |
| 📈 | Benchmarks | Full methodology and numbers |
| 🔄 | State Reset | What gets restored between requests |
| 🔀 | Migrate from nginx | Server blocks, try_files, proxy_pass, gzip |
| 🔀 | Migrate from Apache | .htaccess unchanged, VirtualHost mapping |
| 🔀 | Migrate from Caddy | Automatic HTTPS, on-demand TLS → autohost |
| ✅ | Test Results | 140 end-to-end tests |
| 🗺️ | Roadmap | What's next |
| 📄 | License | MIT |
Quick Start
git clone https://github.com/Qbix/webserver
cd webserver
php qbixserver.php
Or grab a self-contained binary (PHP bundled, nothing to install):
# Linux curl -LO https://github.com/Qbix/webserver/releases/latest/download/qbixserver-linux-x86_64 chmod +x qbixserver-linux-x86_64 ./qbixserver-linux-x86_64 # macOS (Apple Silicon) curl -LO https://github.com/Qbix/webserver/releases/latest/download/qbixserver-macos-arm64 chmod +x qbixserver-macos-arm64 ./qbixserver-macos-arm64 # Windows curl -LO https://github.com/Qbix/webserver/releases/latest/download/qbixserver-windows-x64.exe qbixserver-windows-x64.exe
Use With Your Existing Codebase
If you already have a PHP app running on nginx + php-fpm, switching is one command. The server reads your .htaccess, rewrites URLs to your front controller, and runs your code with 28 functions shimmed so static variables, sessions, and headers work correctly between requests.
Laravel:
cd my-laravel-app
php /path/to/qbixserver.php --root=public --preset=laravel --port=8080
Symfony:
cd my-symfony-app
php /path/to/qbixserver.php --root=public --preset=symfony --port=8080
WordPress:
cd my-wordpress-site
php /path/to/qbixserver.php --root=. --preset=wordpress --port=8080
Drupal:
cd my-drupal-site
php /path/to/qbixserver.php --root=web --preset=drupal --port=8080
Any PHP app with a front controller:
php /path/to/qbixserver.php --root=public --port=8080
If the root directory has an index.php, all clean URLs automatically route to it (the same behavior as try_files $uri $uri/ /index.php in nginx). If there's a .htaccess, its RewriteRule and RewriteCond directives are applied.
What --preset does
Each preset sets framework-appropriate defaults: the front controller path, upload limits, memory limits, and session GC settings. You can override any of these in a JSON config file. The preset is a convenience — without it, the server still works if your .htaccess handles routing.
What gets shimmed
The server intercepts 28 PHP functions (header(), session_start(), setcookie(), ini_set(), etc.) via source transformation at include time. Your code calls header() and it works — the server captures it. Between requests, all static properties are restored from a snapshot in 0.03ms. See Compatibility for the full list.
What to watch for
Most apps work immediately. A few things to be aware of:
define()constants persist between requests in persistent workers. If a plugin defines a constant conditionally, the second request sees it already defined. Rare in practice.stream_wrapper_register()persists. Uncommon outside testing frameworks.- Long-running scripts (migrations, imports) should use
--workers=1or run via CLI directly. - Extensions that store C-level state (e.g. some custom PECL modules) won't reset between requests. Standard extensions (PDO, curl, mbstring) are fine.
Platform Support
| Platform | Workers | COW | Mode |
|---|---|---|---|
| Linux x86_64, aarch64 | pcntl_fork | Yes — 120KB per worker | Persistent or fork-per-request |
| macOS Intel, Apple Silicon | pcntl_fork | Yes | Persistent or fork-per-request |
| FreeBSD | pcntl_fork | Yes | Persistent or fork-per-request |
| Windows x64 | php-cgi subprocess | No | Persistent workers with source-transform shimming |
On Linux and macOS, the server runs thousands of COW-forked workers at ~120KB each. On Windows, pcntl doesn't exist, so the server spawns php-cgi subprocesses for process isolation. Workers are still persistent and shimmed — the same code runs, you just don't get the COW memory savings. The 28-function source transform still clears state between requests.
PHP 8.6+ (epoll/kqueue): The server auto-detects PHP 8.6's native Io\Poll API and uses epoll on Linux or kqueue on macOS for event notification — no PECL extensions needed. On older PHP versions, the server uses stream_select (which works fine, just O(n) per tick instead of O(1)). Revolt is also supported if installed.
Examples
Six example apps are included in examples/:
| App | What it demonstrates |
|---|---|
| todo | SQLite CRUD, REST API, static HTML |
| counter | SQLite persistence, GET/POST |
| chat | WebSocket rooms, Socket.IO, 8 handler files |
| stream | Server-Sent Events, AI token streaming |
| swarm | Q::event() dispatch, cluster replication |
| collab | Collaborative editing |
php qbixserver.php --root=examples/todo/web --port=8080
Migrating from another server
Already running nginx, Apache, or Caddy? These guides show the config mapping:
- Migrating from nginx — server blocks, try_files, proxy_pass, gzip
- Migrating from Apache — .htaccess works unchanged, VirtualHost → domains config
- Migrating from Caddy — automatic HTTPS, on-demand TLS → autohost
Single-Binary Distribution
Package your app into one executable file — PHP runtime, web server, and all your code. The binary includes SQLite auto-provisioning: if your app bundles a .sqlite file, the server copies it to the data directory on first run and writes the framework config to point at it. No external database needed.
Supported out of the box: Qbix (detects plugins, writes local/app.json with per-plugin prefixes), Laravel (.env), Symfony (.env), WordPress (wp-config.php + wp-sqlite-db), Craft CMS, and Drupal.
Sign binaries with ECDSA P-256 keys (M-of-N threshold), publish to Sigstore Rekor for independent verification, and customize by editing the binary as a zip file.
- Building and distributing binaries — pack, sign, verify, customize, platform code signing
License
MIT — use it however you want.
We proposed switch_global_context() for PHP core. While that works its way through the RFC process, the server does it in userland today.