https://qbixserver.com is an all-in-one server that handles everything for you. Drop files in folders. Get real-time applications that can handle millions of users. Produce and distribute standalone binaries that run on Linux, Mac, Windows, and now iOS and Android too. Qbix Server v2 lets you build secure decentralized apps that can even work offline, over WiFi and Bluetooth.
When serving millions of people, safety becomes very important. Learn why the server is written in PHP. Today's PHP ecosystem has also produced thousands of useful web frameworks including OwnCloud, WordPress, Magento, Drupal, Symfony, Laravel, and more. Qbix Server can run them all, unmodified. It also gives you a dashboard and visual control panel to manage all your apps, domains, certificates, etc. in one place.
See BENCHMARKS.md for full methodology and reset.md for what gets restored between requests.
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, image resizing, .htaccess — same port, same file. No Redis, no Node, no pub/sub glue.
In v2.0, the same server also discovers nearby devices over Bluetooth and Wi-Fi, establishes ECDH-encrypted sessions, routes messages through multi-hop mesh, and synchronizes data peer-to-peer with Bloom filters and prolly trees. It runs on iOS and Android alongside Linux, macOS, and Windows. A classroom of phones running the same PHP app, syncing data with no internet, no central server — one php qbixserver.php.
You can also package your entire app — code, assets, SQLite database — into a single 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. How it works →
- Documentation
- Quick Start
- Use With Your Existing Codebase
- Performance
- vs FrankenPHP and Swoole
- Features
- Server Headers
- For PHP Developers
- Configuration
- Platform Support
- Three Ways to Run
- Building
- Examples
- Migrating from another server
- Single-Binary Distribution
- Mesh Networking
- Mobile
- With Qbix Platform
- Architecture
- HTTP/2 Support
- Why PHP
- License
git clone https://github.com/Qbix/webserver
cd webserver
php qbixserver.phpOr 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.exeOr use the PHAR (single file, no extensions to compile):
php bin/qbixserver.phar --root=./public --port=8080If 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 27 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=8080Symfony:
cd my-symfony-app
php /path/to/qbixserver.php --root=public --preset=symfony --port=8080WordPress:
cd my-wordpress-site
php /path/to/qbixserver.php --root=. --preset=wordpress --port=8080Drupal:
cd my-drupal-site
php /path/to/qbixserver.php --root=web --preset=drupal --port=8080Any PHP app with a front controller:
php /path/to/qbixserver.php --root=public --port=8080If 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.
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.
The server intercepts 27 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.
Qbix Server ships presets and boot adapters for 13 frameworks. Every framework runs unmodified — no plugins or code changes needed.
"—" = not benchmarked or no significant speedup on CPU-bound "hello world". Real-world I/O-bound workloads see much larger gains from the COW memory model (see Benchmarks).
See FRAMEWORKS.md for full details, preset reference, and boot adapter documentation. See BENCHMARKS.md for full benchmark methodology and per-framework numbers.
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.
Benchmarked against nginx on the same single-core container, PHP 8.3, Ubuntu 24. 13KB static file, best-of-3 runs, warm caches.
Zero failed requests across 50,000+ requests at concurrency 50. Server never crashed.
For context: 20K req/s means the server handles 1,000 simultaneous page loads per second (assuming ~20 static assets per page), all from a single PHP process.
For PHP application workloads, the story flips — the memory and bootstrap savings matter more than static file throughput:
- nginx + php-fpm: 0.1ms static file + 30ms PHP bootstrap + 5ms actual work = 35ms
- Qbix Server: 0.15ms static file + 0ms bootstrap + 5ms actual work = 5ms
See BENCHMARKS.md for full methodology, and the framework benchmarks for per-framework numbers.
💡 You can always put nginx, a reverse proxy, or a CDN (Cloudflare, CloudFront) in front of this for faster HTTPS and edge caching. Qbix Server handles the PHP execution, access control, and intelligent caching behind it.
If you're looking beyond php-fpm, you've probably seen FrankenPHP and Swoole. Here's how they compare:
FrankenPHP and Swoole keep PHP workers alive across requests. This is fast, but it means global state, static variables, database connections, and in-memory caches persist between unrelated requests. This causes subtle bugs:
// This leaks between requests in FrankenPHP/Swoole:
class UserService {
private static ?User $cached = null;
public static function current(): User {
if (!self::$cached) {
self::$cached = User::fromSession();
}
return self::$cached; // Returns previous user's data!
}
}Every PHP framework, library, and snippet that uses static variables, singletons, or global state becomes a potential security hole. You have to audit everything.
Qbix Server avoids this entirely. Workers fork from a preloaded parent, so they inherit loaded classes and parsed config (read-only, shared via copy-on-write). But each request runs in its own process — when it's done, everything is gone. No state leaks. No audit needed. Your existing PHP code works exactly as it does on php-fpm.
FrankenPHP requires Go tooling to build or a pre-built binary that bundles Caddy. Swoole requires compiling a C extension, which can conflict with other extensions and doesn't work on all hosting environments.
Qbix Server is a PHP file. If you can run php -v, you can run the server. It uses standard PHP extensions (sockets, pcntl) that come pre-installed on most systems. There's no compilation step, no foreign runtime, no binary compatibility issues.
# FrankenPHP
docker pull dunglas/frankenphp # 150MB+ image, or build from Go source
# Swoole
pecl install swoole # compiles C, may fail on some systems
# Then edit php.ini, restart php...
# Qbix Server
php qbixserver.php --port=8080 # doneChoose FrankenPHP if you want Caddy's ecosystem (automatic HTTPS, HTTP/3) and don't mind Go as a dependency. Good for Laravel projects that already use Octane.
Choose Swoole if you need coroutines for high-concurrency I/O (thousands of simultaneous HTTP client requests, database queries). Good for async-heavy microservices.
Choose Qbix Server if you want shared-nothing safety, zero-install deployment, access-controlled file serving, component-level cache invalidation, and full compatibility with existing PHP code. Good for apps that serve pages (not just APIs), need fine-grained caching, and want the simplest possible deployment.
Qbix Server understands special response headers from your PHP scripts. These are the same headers nginx understands (like X-Accel-Redirect) plus new ones for component-level caching. Your PHP sends them with header(), the server acts on them.
All of these are standard PHP header() calls. No SDK, no framework needed. The server strips them before sending the response to the client.
With a typical server, your uploaded files sit at public URLs. Anyone with the link can access them — and share the link with others. The usual workaround is "unguessable" URLs, which are just security through obscurity.
X-Accel-Redirect lets your PHP check access, then tells the server to serve the file directly — fast, streamed, with no public URL exposed:
<?php
// web/download.php — access-controlled file serving
session_start();
$fileId = $_GET['id'] ?? '';
$userId = $_SESSION['user_id'] ?? null;
// Your access control logic
if (!$userId || !userCanAccess($userId, $fileId)) {
http_response_code(403);
echo 'Access denied';
exit;
}
// Tell the server to serve the file directly.
// The client never sees the real path.
header("X-Accel-Redirect: /uploads/private/{$fileId}");
header("Content-Disposition: attachment; filename=\"document.pdf\"");
// The server takes over from here — streams the file
// with correct Content-Type, ETag, compression, etc.
// Your PHP process is already done.No public URL for the file. No redirect the user can bookmark. The server streams the file after your PHP has verified access and exited.
Most caching systems cache whole pages. When anything changes, you throw away the entire page and re-render everything. Qbix Server can cache individual components and only re-render what changed.
Step 1: Register components when rendering a page
<?php
// web/community.php — a page with three components
$feedHtml = renderFeed($communityId);
$sidebarHtml = renderSidebar($communityId);
$membersHtml = renderMembers($communityId);
// Tell the server about the component tree and what data each depends on
header('X-Cache-Tree: ' . json_encode([
'l' => [
'feed' => md5($feedHtml),
'sidebar' => md5($sidebarHtml),
'members' => md5($membersHtml),
]
]));
header('X-Cache-Deps: ' . json_encode([
'feed' => ["community/{$communityId}/feed"],
'sidebar' => ["community/{$communityId}/about"],
'members' => ["community/{$communityId}/participants"],
]));
header('Cache-Control: public, max-age=300');
echo $feedHtml . $sidebarHtml . $membersHtml;Step 2: Invalidate when data changes
<?php
// web/post.php — user posts to the feed
saveNewPost($communityId, $content);
// Tell the server which dependency key changed
header('X-Cache-Invalidate: ' . json_encode([
"community/{$communityId}/feed"
]));
// The server walks its dependency graph:
// community/123/feed → page /community/123 component 'feed'
// Only 'feed' is stale. Sidebar, members = still cached.
// Next request re-renders only the feed component.
echo json_encode(['ok' => true]);The server maintains a Merkle tree of component hashes. When a dependency key is invalidated, it walks the tree to find exactly which components on which pages are affected. Everything else is served from the in-memory cache.
Qbix Server isn't just a static file server with PHP bolted on. It's a micro-framework where you drop files into conventional directories and things just work — classes autoload, events fire handlers, views render templates. No configuration needed for the basics.
myproject/
├── qbixserver.php ← server entry point (or use the PHAR)
├── config/
│ └── server.json ← server + app configuration
├── web/ ← document root (publicly accessible)
│ ├── index.html ← static files served directly
│ ├── style.css
│ ├── api.php ← PHP scripts executed on request
│ └── uploads/
├── classes/ ← your PHP classes (autoloaded when first used)
│ ├── MyApp/
│ │ ├── User.php ← MyApp\User or MyApp_User
│ │ ├── Feed.php
│ │ └── Auth.php
│ └── vendor/
│ └── autoload.php ← Composer autoloader (optional)
├── handlers/ ← event handlers (loaded on demand)
│ └── MyApp/
│ └── feed/
│ ├── post.php ← handles "MyApp/feed/post" event
│ └── validate.php ← handles "MyApp/feed/validate" event
└── views/ ← PHP templates for Q::view()
└── MyApp/
└── feed/
├── page.php
└── item.php
Only web/ is accessible via HTTP. Everything else is server-side only.
Your PHP scripts don't need to require or include anything. The server has already loaded the Q class, the autoloader, and the event system before your script runs. Classes from classes/, events via Q::event(), views via Q::view() — all available immediately:
<?php
// web/api.php — no require, no include, no bootstrap
use MyApp\User;
$user = User::find($_GET['id']);
$feed = Q::event('MyApp/feed/get', ['userId' => $user->id]);
header('Content-Type: application/json');
echo json_encode($feed);Drop a file in handlers/ and it's available as an event:
<?php
// handlers/MyApp/feed/post.php
function MyApp_feed_post(&$params, &$result) {
$title = $params['title'] ?? 'Untitled';
$userId = $params['userId'] ?? null;
$id = saveFeedPost($userId, $title);
$result = ['id' => $id, 'title' => $title, 'saved' => true];
return $result;
}<?php
// web/api.php — fire it from anywhere
$result = Q::event('MyApp/feed/post', [
'title' => $_POST['title'],
'userId' => $_SESSION['user_id'],
]);
header('Content-Type: application/json');
echo json_encode($result);The handler file is include'd the first time the event fires, then the function stays in memory. If the event never fires, the file is never loaded.
Before/after hooks via config — useful for validation, logging, access control:
{
"Q": {
"handlersBeforeEvent": {
"MyApp/feed/post": ["MyApp/feed/validate"]
},
"handlersAfterEvent": {
"MyApp/feed/post": ["MyApp/feed/notify"]
}
}
}Any before hook returning false stops the chain. Handlers can also be URLs — the server POSTs event parameters as JSON to remote endpoints, giving you webhooks built into the event system.
Classes are eager. Handlers are lazy. Scripts are per-request. Views are on-demand. This gives you the right loading strategy for each kind of code without thinking about it — just put files in the right directory.
When your project outgrows the micro-framework and you need user accounts, real-time streams, access control, payments, or a plugin system, switch to --app mode and everything you've written keeps working. See With Qbix Platform.
Create config/server.json next to your web/ directory, or pass --config=path/to/config.json:
{
"Q": {
"webserver": {
"keepAlive": {
"max": 100,
"timeout": 15
},
"maxConnections": 1024,
"fileCache": {
"maxSize": 67108864,
"maxFile": 1048576,
"checkInterval": 1
},
"rateLimit": {
"enabled": true,
"requests": 100,
"window": 60
}
}
}
}See Configuration for the full reference.
On Linux, macOS and BSD, the server runs thousands of COW-forked workers at ~120KB each. On Windows, the server ships qbix_fork.dll which uses RtlCloneUserProcess via PHP's FFI extension to give full COW fork semantics — same memory efficiency as POSIX platforms. If FFI or the DLL isn't available, the server falls back to php-cgi subprocesses (no COW, higher memory per worker, but fully functional). On iOS and Android, the PHP runtime is embedded in a native app shell; the TransportManager handles peer discovery and transport negotiation automatically.
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.
GitHub Actions CI builds and tests on Linux x86_64, Linux aarch64, macOS arm64, Windows x64, FreeBSD 14, plus experimental Android and iOS targets. 464 tests, 0 failures.
Linux / macOS (recommended):
- PHP 8.1 or later
- Extensions: sockets,pcntl(for signals + workers),openssl(for HTTPS)
# Check
php -m | grep -E 'sockets|pcntl|openssl'
# Install on Ubuntu/Debian
sudo apt install php-cli php-socketsFor the static binary: Nothing. The PHP runtime is included.
Windows: The server runs in single-threaded mode (--workers=0 only) unless qbix_fork.dll + FFI is available. Static files, PHP scripts, WebSocket, caching, compression, access control — everything works. You lose fork-per-request isolation and signal-based graceful shutdown without pcntl. Good for development; for production use Linux or macOS (or WSL).
php qbixserver.php --root=./web --port=8080php bin/qbixserver.phar --root=./web --port=8080
# Or make it executable
chmod +x bin/qbixserver.phar
./bin/qbixserver.phar --port=8080# Download from GitHub Releases
chmod +x qbixserver-linux-x86_64
./qbixserver-linux-x86_64 --root=./web --port=8080The binary bundles PHP 8.3 + extensions into a single ~15MB executable. Copy it to any Linux or macOS machine and run. No dependencies.
php -d phar.readonly=0 build-phar.php
# Output: bin/qbixserver.phar# With Docker (easiest):
./build-binary.sh --docker
# With static-php-cli installed locally:
./build-binary.sh
# Output: bin/qbixserver (~15MB)The binary is built using static-php-cli, which compiles PHP + extensions into a statically linked binary.
GitHub Actions automatically builds binaries for Linux x86_64, Linux ARM64, macOS x86_64, and macOS Apple Silicon on every tagged release.
Six example apps are included in examples/:
php qbixserver.php --root=examples/todo/web --port=8080Already 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
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
Every Qbix Server instance has a cryptographic identity (ECDSA P-256, same security model as Ethereum). When two servers discover each other — over Bluetooth, Wi-Fi, or TCP — they perform an ECDH handshake and establish an AES-256-GCM encrypted session. All traffic is encrypted end-to-end, even through relay nodes.
// Talk to a nearby server (transport is automatic)
$response = Q::handleUsingRemote('qbix-peer://' . $peerId . '/api/data');
// React to peers
Q_WebServer_Transport::onPeerOnline(function ($peer) {
// Sync data, exchange messages, coordinate
});Multi-hop routing extends range beyond direct connections. The router uses distance-vector routing with HELLO/BYE/HEARTBEAT propagation, TTL limits, and deduplication. Intermediate nodes relay encrypted payloads they cannot read.
Data sync runs automatically when peers connect: Bloom filter exchange identifies what's different, then only the missing records transfer. For large datasets (10,000+ records), the protocol switches to prolly tree comparison — a deterministic content-addressed tree where identical subtrees are skipped entirely.
See docs/Mesh.md for the full protocol specification, edge cases, and security analysis.
Qbix Server runs on iOS and Android as a native app. A Swift (iOS) or Kotlin (Android) shell starts the embedded PHP server on 127.0.0.1, points a WebView at it, and handles peer-to-peer transport. The PHP process runs your Qbix app exactly as it would on a desktop or VPS — no Cordova, no Capacitor, no JavaScript bridge.
The native TransportManager discovers nearby peers over every available channel and picks the best transport automatically:
If Wi-Fi drops, traffic falls back to BLE seamlessly. The PHP server sees HTTP on localhost regardless of transport.
Background persistence: iOS uses a silent AVAudioEngine session (App Store precedent: PocketServer, BitChat). Android uses a Foreground Service with START_STICKY.
In the control panel, select your app and click Prepare with the iOS or Android platform selected. This generates a native project scaffold under apps/YourApp/mobile/ios/ or apps/YourApp/mobile/android/ with all the Swift/Kotlin source files, manifest, build config, and a Server/ (iOS) or assets/ (Android) directory where you place the PHP binary or phar.
- Get the qbixserver-ios-arm64micro binary from the CI release artifacts, or build it locally with static-php-cli. For Simulator testing, just placeqbixserver.pharin the Server directory — PhpBridge falls back to the system PHP on macOS.
- Generate the Xcode project: cd apps/YourApp/mobile/ios && xcodegen generate
- Open the .xcodeproj, set your signing team, and build.
For distribution, archive in Xcode and upload to App Store Connect via the Organizer or xcodebuild -exportArchive. See mobile/iOS.md for the full walkthrough including signing, TestFlight, and App Store submission.
- Get the qbixserver-android-arm64micro binary from the CI release artifacts, or cross-compile it locally with static-php-cli and the Android NDK. For debug testing,qbixserver.pharalso works.
- Place the binary (or phar) in app/src/main/assets/.
- Build: cd apps/YourApp/mobile/android && ./gradlew assembleDebug(orbundleReleasefor Play Store).
For distribution, sign the release AAB and upload to the Google Play Console. See mobile/Android.md for the full walkthrough including keystore setup, signing, and Play Store submission.
- mobile/README.md — transport layer architecture, BLE GATT service definition, chunking protocol, platform requirements
- mobile/iOS.md — PhpBridge, xcodegen, background persistence, signing, TestFlight, App Store
- mobile/Android.md — PhpBridge, Gradle, Foreground Service, signing, Play Store
Qbix Server is extracted from the Qbix Platform — a full-stack framework for building social apps with real-time streams, user management, and plugin architecture.
When you have a Qbix app, the server uses the full framework:
php qbixserver.php --app=/path/to/myapp --port=8080In this mode:
- Requests route through Q_Dispatcher— the full Qbix event pipeline
- Plugins load automatically (Users, Streams, Assets, etc.)
- Clean URLs work (/community/123→ module routing)
- Static files still use the fast path (no framework overhead)
- The dashboard shows Qbix-specific stats
The standalone mode (without --app) runs as a plain web server — no framework, no plugins. PHP files execute directly, static files serve from memory. Use this for simple sites, APIs, or any project that doesn't need the full Qbix stack.
See --app Mode & SAPI Internals for details.
┌──────────────────┐
HTTP request ────→ │ Event Loop │ stream_select (zero deps)
│ (single thread) │ or amphp/revolt (optional)
└────────┬─────────┘
│
┌───────────────┼───────────────┐
│ │ │
┌────▼─────┐ ┌────▼─────┐ ┌────▼─────┐
│ Static │ │ PHP │ │ WebSocket │
│ Files │ │ Dispatch │ │ Upgrade │
│ │ │ │ │ │
│ In-memory│ │ In-proc │ │ RFC 6455 │
│ response │ │ or fork │ │ frames │
│ cache │ │ pool │ │ │
└──────────┘ └──────────┘ └──────────┘
Static files are served from an in-memory response cache. The full HTTP response (headers + body) is pre-built and sent in a single fwrite() call. The cache is mtime-validated with configurable check intervals. Combined with TCP_NODELAY, this delivers sub-millisecond response times.
PHP scripts run in-process (single-threaded, suitable for lightweight APIs) or in a pre-fork worker pool (--workers=N) for concurrent PHP execution. Workers are forked after class preloading, so they share the base memory footprint via copy-on-write pages.
The remaining gap versus nginx (55–73%) is inherent: nginx uses sendfile() (kernel-space file→socket copy), epoll (O(1) event notification), and compiled C. PHP's stream_select is select(2), file serving goes through userspace, and every operation has interpreter overhead. Getting to 55–73% of C performance from pure interpreted PHP is about as good as it gets.
The built-in event loop uses stream_select — zero dependencies, works everywhere. But if you install amphp, the server upgrades to a full HTTP/2 server with no code changes:
composer require amphp/http-server amphp/socket
php qbixserver.php --port=8443The server has a clean two-layer architecture. Q_WebServer::route() handles all request logic (static files, PHP dispatch, cache, access control) and returns a [status, headers, body] array. The transport layer is pluggable — both transports use the same routing, caching, and access control logic.
Either way: You can always put Cloudflare, CloudFront, or nginx in front as a reverse proxy. The CDN terminates HTTP/2 (and HTTP/3) for you, forwarding HTTP/1.1 to the backend.
Every web server faces a tradeoff between performance and isolation. Persistent-process servers (Node, Go, Java, Python WSGI, PHP-FPM, Swoole) keep workers alive across requests — fast, but memory leaks accumulate and one request's data (including secrets) can reach another through shared address space. Process-per-request servers (CGI) give each request a clean process — safe, but each process costs 30–60MB and takes milliseconds to start.
The only OS mechanism that eliminates this tradeoff is copy-on-write fork(): the parent pre-loads the application, each request gets a forked child with its own address space, and the kernel shares memory pages until the child writes to them. Per-worker cost: ~120KB. Startup time: microseconds. Isolation: absolute — the child's address space is reclaimed by the OS when it exits, so leaks can't accumulate and secrets can't cross requests.
COW fork requires an interpreter with a compact heap and shared-nothing per-request semantics. PHP is the only major language where both properties hold, and millions of existing web applications already assume the one-request-then-die model. That's why this server is written in PHP. It's able to run thousands of existing web apps including OwnCloud, WordPress, Magento, Drupal, Symfony, Laravel, and Qbix.
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. Qbix is a pure PHP server. Each request runs in its own process via COW fork — OS-enforced isolation means memory leaks can't accumulate and secrets can't leak between requests, at 120KB per worker instead of 50MB.
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.
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. 27 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.
The fork model gives you something no persistent-process server can: hardware-enforced isolation between requests.
Memory leaks can't accumulate. In Swoole, RoadRunner, or any persistent-process server (Node, Go, Python WSGI), a memory leak in request handling grows with every request until the worker is recycled. If a Laravel controller allocates an array it forgets to unset, that memory stays allocated for the next request, and the next, and the next. Worker recycling limits how bad it gets, but it doesn't prevent it. In Qbix Server, the child process handles one request and calls exit(). The OS reclaims the entire address space. There is no "next request" in the same process. The parent's memory is untouched — COW means the child's writes never propagate back.
Secrets can't leak across requests. In a persistent-process server, request A's variables live in the same address space as request B. If request A processes a credit card number and the handler has a bug — doesn't clear the variable, stores it in a class property, logs it to a debug buffer — request B's handler can read it. A memory dump contains both requests' data intermixed. With fork-per-request, request A runs in process 17432 and request B runs in process 17433. They have separate virtual address spaces. There is no mechanism — no bug, no misconfiguration, no race condition — by which B can read A's memory, because A's address space was reclaimed by the kernel when the process exited. This is the same isolation principle behind Chrome's site isolation and why operating systems use separate address spaces for separate users.
Every other PHP execution model makes you choose: FastCGI/Swoole/RoadRunner give you performance but sacrifice isolation, traditional CGI gives you isolation but sacrifices performance. The COW fork trick is the only architecture that provides both — the performance of a persistent server (pre-loaded parent, microsecond fork, 120KB per worker) with the safety of CGI (each request gets a clean process that dies after).
This is the real argument for security-conscious deployments. You can run untrusted PHP plugins, WordPress with unaudited third-party themes, legacy code nobody has reviewed — and a bug in one request's handling cannot affect another request's data because the OS enforces the boundary.
MIT — use it however you want.
Part of the Qbix Platform.
We proposed switch_global_context() for PHP core. While that works its way through the RFC process, the server does it in userland today.