# β‘ Qbix Server
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.
**55β73% of nginx throughput** on static files. Zero dependencies beyond PHP itself.
---
## π Table of Contents
- [Quick Start](#-quick-start)
- [Performance](#-performance)
- [Why Not php-fpm?](#-why-not-php-fpm)
- [Features](#-features)
- [Server Powers](#-server-powers--what-your-php-can-do)
- [Configuration](#-configuration)
- [PHP Scripts](#-php-scripts)
- [Three Ways to Run](#-three-ways-to-run)
- [Building](#-building)
- [With Qbix Platform](#-with-qbix-platform)
- [Architecture](#-architecture)
- [Requirements](#-requirements)
- [License](#-license)
---
## π Quick Start
```bash
# Clone
git clone https://github.com/Qbix/Server.git
cd Server
# Create a web directory
mkdir web
echo '
Hello World
' > web/index.html
# Run
php server.php --port=8080
```
Open [http://localhost:8080](http://localhost:8080). That's it.
```bash
# Or serve an existing directory
php server.php --root=/var/www/mysite --port=80
# Or use the PHAR (single file, 196KB)
php bin/qbix-server.phar --root=./public --port=8080
```
---
## π Performance
Benchmarked against nginx on the same single-core container, PHP 8.3, Ubuntu 24.
13KB static file, best-of-3 runs, warm caches.
| Scenario | nginx | Qbix Server | Ratio |
|---|---|---|---|
| Sequential (c=1) | 10,154 req/s | 6,376 req/s | **63%** |
| Concurrent (c=10) | 12,300 req/s | 6,876 req/s | **56%** |
| High concurrency (c=50) | 12,919 req/s | 7,253 req/s | **56%** |
| Keep-alive (c=10) | 26,858 req/s | 19,700 req/s | **73%** |
| Keep-alive (c=50) | 30,158 req/s | 20,369 req/s | **67%** |
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.
---
## ποΈ Why Not php-fpm?
The traditional stack β nginx + php-fpm β works like this:
```
Request β nginx β FastCGI socket β php-fpm worker
β
Load PHP
Include autoloader
Boot framework
Connect to DB
Run your code
Send response
β
Worker resets or dies
```
Every PHP request pays the bootstrap cost. Even with OPcache, each php-fpm worker re-initializes your framework's class instances, config trees, and DB connections on every request. For a framework like Qbix (or Laravel, Symfony, etc.), this bootstrap takes **10β50ms** β often longer than the actual work.
**Qbix Server eliminates this entirely:**
```
Startup:
1. Load PHP
2. Include autoloader
3. Load ALL framework classes into memory
4. Parse ALL config files
5. Connect to database
6. pcntl_fork() β workers inherit everything
β
Request:
Worker already has classes, config, DB connections.
Just run your code. 0ms bootstrap.
```
The key insight is **fork after preload**. Unix `fork()` uses copy-on-write, so forked workers share the parent's memory pages for all those preloaded classes. Each worker starts with ~30MB shared (read-only) and allocates only the per-request data. Compare this to php-fpm where each worker loads everything independently, using 30MB Γ N workers of duplicated memory.
### Preloading classes
Use the `--workers=N` flag and configure which classes to preload:
```json
{
"Q": {
"webserver": {
"preload": [
"Q_Dispatcher", "Q_Request", "Q_Response",
"Q_Config", "Q_Cache", "Q_Session",
"Db", "Db_Mysql", "Db_Row", "Db_Query",
"Users", "Users_User", "Users_Session",
"Streams", "Streams_Stream", "Streams_Message"
]
}
}
}
```
```bash
# Start with 4 workers (classes loaded once, shared across all)
php server.php --app=/path/to/myapp --port=8080 --workers=4
```
The parent process loads and parses every class in the `preload` list, then forks. Workers inherit the entire loaded state β OPcache entries, class definitions, parsed config trees, autoloader maps. The first PHP request in each worker runs at full speed, no cold start.
### The numbers
| | php-fpm | Qbix Server |
|---|---|---|
| Bootstrap per request | 10β50ms | **0ms** |
| Memory per worker | 30β60MB each | 30MB shared + ~5MB per worker |
| IPC overhead | FastCGI socket + serialization | Direct function call or Unix fork |
| Static files | Separate nginx process | Same process, memory-cached, single `fwrite` |
| Config reload | Restart all workers | `SIGHUP`, zero downtime |
| WebSocket | Needs separate server | Built in |
For a Qbix app with 20 loaded plugins, the bootstrap savings alone make the server **2β5x faster** on PHP requests compared to nginx + php-fpm.
### Why it's actually faster in practice
The benchmarks above measure static file throughput, where nginx's C implementation and `sendfile()` syscall give it an inherent edge. But for **real PHP applications**, the story flips:
- **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**
The 0.05ms you lose on static files, you gain back 30ms on every PHP request. And you can always put nginx or a CDN in front for the static file edge.
---
## β¨ Features
| Category | What you get |
|---|---|
| **Static files** | ETag, 304 Not Modified, Last-Modified, MIME type detection, in-memory response cache |
| **Keep-alive** | HTTP/1.0 and 1.1, TCP_NODELAY, configurable limits |
| **PHP execution** | `.php` files in document root run in-process or via pre-fork worker pool |
| **Compression** | On-the-fly gzip/brotli + pre-compressed `.gz`/`.br` siblings |
| **WebSocket** | RFC 6455 upgrade on any path |
| **Dashboard** | Live stats at `/Q/dashboard` β request rates, memory, status codes |
| **Health check** | JSON at `/Q/health` β for load balancers and monitoring |
| **Control panel** | Password-protected at `/Q/panel` β manage apps and scripts |
| **Rate limiting** | Per-IP with configurable windows and burst limits |
| **Security** | Path traversal blocked, dotfiles blocked, 431 for oversized headers, 400 for malformed requests |
| **Graceful shutdown** | SIGTERM/SIGINT drain in-flight requests before closing |
| **TLS** | Optional HTTPS with auto-certbot or manual certs |
| **Logging** | Colored terminal output + file-based access logs |
| **Access control** | X-Accel-Redirect support β PHP enforces access, server serves the file |
| **Component cache** | X-Cache-Tree headers β invalidate parts of a page, not the whole thing |
---
## π Server Powers β What Your PHP Can Do
Qbix Server understands special response headers from your PHP scripts, giving you
capabilities that normally require complex nginx configurations or aren't possible at all.
### Access-controlled static files
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.
Qbix Server supports `X-Accel-Redirect`: your PHP checks access, then tells the server
to serve the file directly β fast, streamed, with no public URL exposed:
```php
[
'feed' => $feedHash,
'sidebar' => $sidebarHash,
'members' => $membersHash,
]
]));
header('X-Cache-Deps: ' . json_encode([
'feed' => ["community/{$communityId}/feed"],
'sidebar' => ["community/{$communityId}/about"],
'members' => ["community/{$communityId}/participants"],
]));
// When someone posts to the feed, only 'feed' is invalidated.
// The sidebar and members list are still served from cache.
// The server re-renders only the stale component.
```
When data changes, tell the server which dependency key was affected:
```php
true,
'maxAge' => 300,
'mustRevalidate' => true,
]);
```
The Platform's Streams plugin automatically invalidates cache dependencies when
stream data changes β posts, relations, participant joins β so cached pages
update themselves without manual invalidation calls. Combined with the server's
Merkle tree, this gives you fine-grained, data-driven cache invalidation across
your entire app, with zero configuration.
---
## βοΈ Configuration
Create `config/server.json` next to your `web/` directory, or pass `--config=path/to/config.json`:
```json
{
"Q": {
"webserver": {
"keepAlive": {
"max": 100,
"timeout": 15
},
"maxConnections": 1024,
"fileCache": {
"maxSize": 67108864,
"maxFile": 1048576,
"checkInterval": 1
},
"rateLimit": {
"enabled": true,
"requests": 100,
"window": 60
}
}
}
}
```
| Key | Default | What it does |
|---|---|---|
| `keepAlive.max` | 100 | Max requests per keep-alive connection |
| `keepAlive.timeout` | 15 | Seconds before closing idle connection |
| `maxConnections` | 1024 | Max simultaneous connections |
| `fileCache.maxSize` | 64MB | Total memory for cached file responses |
| `fileCache.maxFile` | 1MB | Largest file to cache in memory |
| `fileCache.checkInterval` | 1 | Seconds between file modification checks |
| `rateLimit.enabled` | false | Enable per-IP rate limiting |
| `rateLimit.requests` | 100 | Requests per window |
| `rateLimit.window` | 60 | Window in seconds |
---
## π PHP Scripts
Any `.php` file in your document root is executed when requested:
```
web/
index.html β served as static file
style.css β served as static file
api.php β executed as PHP
webhook.php β executed as PHP
```
PHP scripts have full access to `$_SERVER`, `$_GET`, `$_POST`, `$_REQUEST`:
```php
date('c'),
'method' => $_SERVER['REQUEST_METHOD'],
'query' => $_GET,
]);
```
For concurrent PHP execution, use `--workers=N` to pre-fork a worker pool.
---
## π¦ Three Ways to Run
### 1. From source (needs PHP 8.1+)
```bash
php server.php --root=./web --port=8080
```
### 2. PHAR β single 196KB file (needs PHP)
```bash
php bin/qbix-server.phar --root=./web --port=8080
# Or make it executable
chmod +x bin/qbix-server.phar
./bin/qbix-server.phar --port=8080
```
### 3. Static binary β no PHP needed
```bash
# Download from GitHub Releases
chmod +x qbix-server-linux-x86_64
./qbix-server-linux-x86_64 --root=./web --port=8080
```
The binary bundles PHP 8.3 + extensions into a single ~15MB executable.
Copy it to any Linux or macOS machine and run. No dependencies.
---
## π¨ Building
### Build the PHAR
```bash
php -d phar.readonly=0 build-phar.php
# Output: bin/qbix-server.phar
```
### Build the static binary
```bash
# With Docker (easiest):
./build-binary.sh --docker
# With static-php-cli installed locally:
./build-binary.sh
# Output: bin/qbix-server (~15MB)
```
The binary is built using [static-php-cli](https://github.com/crazywhalecc/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.
---
## π With Qbix Platform
Qbix Server is extracted from the [Qbix Platform](https://github.com/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:
```bash
php server.php --app=/path/to/myapp --port=8080
```
In 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.
### Qbix Platform scripts
The full Platform includes additional server scripts like `static.php` for
CDN-style static file serving with versioned URLs. See the
[Platform repository](https://github.com/Qbix/Platform) for details.
---
## ποΈ Architecture
```
ββββββββββββββββββββ
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.
---
## π Requirements
**For server.php and PHAR:**
- PHP 8.1 or later
- Extensions: `sockets`, `pcntl` (for signals + workers), `openssl` (for HTTPS)
```bash
# Check
php -m | grep -E 'sockets|pcntl|openssl'
# Install on Ubuntu/Debian
sudo apt install php-cli php-sockets
```
**For the static binary:**
- Nothing. The PHP runtime is included.
---
## π License
MIT β see [LICENSE](LICENSE).
Part of the [Qbix Platform](https://github.com/Qbix/Platform).