High-Performance Servers
BEAR.Sunday applications can run on high-performance PHP servers that eliminate per-request bootstrap overhead. This guide covers three server options: Swoole, RoadRunner, and FrankenPHP.
Overview
In traditional PHP-FPM, each request bootstraps the entire application:
Request -> Boot Framework -> Route -> Execute -> Response -> Shutdown
Request -> Boot Framework -> Route -> Execute -> Response -> Shutdown
Request -> Boot Framework -> Route -> Execute -> Response -> Shutdown
With persistent worker mode, the application boots once:
Boot Framework (once)
|
Request -> Route -> Execute -> Response
Request -> Route -> Execute -> Response
Request -> Route -> Execute -> Response
This eliminates boot overhead, resulting in significantly lower latency and higher throughput.
BEAR.Sunday’s stateless resource design and immutable architecture are well-suited for persistent worker environments, enabling seamless transition to worker mode without global state issues.
Server Comparison
| Feature | Swoole | RoadRunner | FrankenPHP |
|---|---|---|---|
| Language | C + PHP | Go + PHP | Go + PHP |
| Worker Mode | Yes | Yes | Yes |
| HTTP/2 | Yes | Yes | Yes |
| HTTP/3 | No | No | Yes |
| WebSocket | Native | Native | Via Caddy |
| Coroutines | Yes | No | No |
| Hot Reload | Manual | Yes | Yes |
| Memory Limit | Shared | Per worker | Per worker |
Quick Start with Docker
The bear-sunday-servers repository provides ready-to-use Docker configurations for all three servers.
git clone https://github.com/bearsunday/bear-sunday-servers.git
cd bear-sunday-servers
# Swoole (port 8081)
cd swoole && docker compose up -d && curl http://localhost:8081/
# RoadRunner (port 8082)
cd roadrunner && docker compose up -d && curl http://localhost:8082/
# FrankenPHP (port 8080)
cd frankenphp && docker compose up -d && curl http://localhost:8080/
Swoole
Swoole is a coroutine-based PHP extension providing event-driven asynchronous I/O.
Features
- Event-Driven: Asynchronous I/O handling
- Coroutines: Concurrent request processing without threads
- High Performance: Eliminates per-request boot overhead
- Memory Efficient: Shared memory between workers
Install
Swoole Extension
pecl install swoole
Or compile from source:
git clone https://github.com/swoole/swoole-src.git && \
cd swoole-src && \
phpize && \
./configure && \
make && make install
Add extension=swoole.so to your php.ini.
BEAR.Swoole Package
composer require bear/swoole
Bootstrap Script
Create bin/swoole.php:
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/autoload.php';
$bootstrap = dirname(__DIR__) . '/vendor/bear/swoole/bootstrap.php';
$context = getenv('BEAR_CONTEXT') ?: 'prod-hal-app';
$ip = getenv('SWOOLE_IP') ?: '0.0.0.0';
$port = (int) (getenv('SWOOLE_PORT') ?: 8080);
exit((require $bootstrap)(
$context,
'MyVendor\MyProject',
$ip,
$port
));
Run
php bin/swoole.php
Swoole http server is started at http://127.0.0.1:8080
Environment Variables
| Variable | Default | Description |
|---|---|---|
BEAR_CONTEXT |
prod-hal-app | BEAR.Sunday context |
SWOOLE_IP |
0.0.0.0 | Server bind address |
SWOOLE_PORT |
8080 | Server port |
Architecture
Master Process
|
+-- Manager Process
|
+-- Worker 1 (coroutines)
+-- Worker 2 (coroutines)
+-- Worker N (coroutines)
Each worker can handle multiple concurrent requests using coroutines.
Development Notes
Xdebug is not fully compatible with Swoole’s coroutines. For debugging:
- Use
var_dump()/error_log()for simple debugging - Or disable Swoole and use PHP’s built-in server with Xdebug
Swoole does not support automatic hot reload. Restart after code changes:
# With Docker
docker compose restart
# Without Docker
pkill -f swoole.php && php bin/swoole.php
RoadRunner
RoadRunner is a high-performance Go application server with PSR-7 PHP workers.
Features
- Go Application Server: High-performance process manager
- PSR-7 Workers: Standard HTTP message interface
- Built-in Metrics: Prometheus-compatible endpoint
- Hot Reload: Automatic worker restart on file changes
Install
RoadRunner Binary
Download from releases or use Docker.
PHP Dependencies
composer require spiral/roadrunner-http nyholm/psr7
Configuration
Create .rr.yaml:
version: "3"
server:
command: "php bin/worker.php"
relay: pipes
http:
address: "0.0.0.0:8082"
pool:
num_workers: 4
max_jobs: 1000
allocate_timeout: 60s
destroy_timeout: 60s
logs:
mode: production
level: info
output: stdout
status:
address: "0.0.0.0:2112"
Worker Script
Create bin/worker.php:
<?php
declare(strict_types=1);
use BEAR\Resource\ResourceObject;
use BEAR\Sunday\Extension\Application\AppInterface;
use MyVendor\MyProject\Injector;
use MyVendor\MyProject\Module\App;
use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Message\ServerRequestInterface;
use Spiral\RoadRunner\Http\PSR7Worker;
use Spiral\RoadRunner\Worker;
require dirname(__DIR__) . '/autoload.php';
// Get configuration from environment
$context = getenv('BEAR_CONTEXT') ?: 'prod-hal-app';
$maxRequests = (int) (getenv('MAX_REQUESTS') ?: 1000);
// Boot application once (outside the request loop)
$app = Injector::getInstance($context)->getInstance(AppInterface::class);
assert($app instanceof App);
// Create RoadRunner worker
$worker = Worker::create();
$factory = new Psr17Factory();
$psr7Worker = new PSR7Worker($worker, $factory, $factory, $factory);
$requestCount = 0;
while ($psrRequest = $psr7Worker->waitRequest()) {
try {
if (! $psrRequest instanceof ServerRequestInterface) {
break;
}
// Convert PSR-7 request to $_SERVER format
$server = createServerVars($psrRequest);
$globals = createGlobals($psrRequest);
// Route and execute request
$request = $app->router->match($globals, $server);
$response = $app->resource->{$request->method}->uri($request->path)($request->query);
assert($response instanceof ResourceObject);
// Convert ResourceObject to PSR-7 Response
$psrResponse = $factory->createResponse($response->code);
foreach ($response->headers as $name => $value) {
$psrResponse = $psrResponse->withHeader($name, $value);
}
$psrResponse = $psrResponse->withBody($factory->createStream((string) $response));
$psr7Worker->respond($psrResponse);
} catch (Throwable $e) {
$psr7Worker->respond($factory->createResponse(500)->withBody(
$factory->createStream($e->getMessage())
));
}
gc_collect_cycles();
$requestCount++;
if ($maxRequests > 0 && $requestCount >= $maxRequests) {
break;
}
}
function createServerVars(ServerRequestInterface $request): array
{
$uri = $request->getUri();
$server = $request->getServerParams();
$server['REQUEST_METHOD'] = $request->getMethod();
$server['REQUEST_URI'] = $uri->getPath() . ($uri->getQuery() ? '?' . $uri->getQuery() : '');
$server['QUERY_STRING'] = $uri->getQuery();
$server['HTTP_HOST'] = $uri->getHost();
foreach ($request->getHeaders() as $name => $values) {
$key = 'HTTP_' . strtoupper(str_replace('-', '_', $name));
$server[$key] = implode(', ', $values);
}
return $server;
}
function createGlobals(ServerRequestInterface $request): array
{
return [
'_GET' => $request->getQueryParams(),
'_POST' => (array) $request->getParsedBody(),
'_COOKIE' => $request->getCookieParams(),
'_FILES' => $request->getUploadedFiles(),
'_SERVER' => createServerVars($request),
];
}
Run
./rr serve -c .rr.yaml
Environment Variables
| Variable | Default | Description |
|---|---|---|
BEAR_CONTEXT |
prod-hal-app | BEAR.Sunday context |
MAX_REQUESTS |
1000 | Requests before worker restart |
Architecture
RoadRunner (Go)
|
+-- PHP Worker 1 (persistent)
+-- PHP Worker 2 (persistent)
+-- PHP Worker N (persistent)
Each worker boots BEAR.Sunday once and handles requests via pipes.
Metrics
Prometheus metrics available at http://localhost:2112/metrics.
FrankenPHP
FrankenPHP is a modern PHP application server built on Caddy with worker mode support.
Features
- Worker Mode: Eliminates application boot cost per request
- HTTP/2 & HTTP/3: Automatic HTTPS with Caddy
- Production Ready: OPcache JIT, multi-stage builds
- Development Ready: Xdebug, hot reload
Install
FrankenPHP is typically used via Docker. For standalone installation, see FrankenPHP documentation.
Worker Script
Create bin/worker.php:
<?php
declare(strict_types=1);
use BEAR\Resource\ResourceObject;
use BEAR\Sunday\Extension\Application\AppInterface;
use MyVendor\MyProject\Injector;
use MyVendor\MyProject\Module\App;
require dirname(__DIR__) . '/autoload.php';
// Get configuration from environment
$context = getenv('BEAR_CONTEXT') ?: 'prod-hal-app';
$maxRequests = (int) (getenv('MAX_REQUESTS') ?: 1000);
// Boot application once (outside the request loop)
$app = Injector::getInstance($context)->getInstance(AppInterface::class);
assert($app instanceof App);
$requestCount = 0;
// FrankenPHP worker loop
// Superglobals ($_GET, $_POST, $_SERVER) are automatically reset
do {
$running = frankenphp_handle_request(static function () use ($app): void {
try {
// Check HTTP cache
if ($app->httpCache->isNotModified($_SERVER)) {
$app->httpCache->transfer();
return;
}
// Route and execute request
$request = $app->router->match($GLOBALS, $_SERVER);
$response = $app->resource->{$request->method}->uri($request->path)($request->query);
assert($response instanceof ResourceObject);
$response->transfer($app->responder, $_SERVER);
} catch (Throwable $e) {
$app->throwableHandler->handle($e, $request ?? null)->transfer();
}
gc_collect_cycles();
});
$requestCount++;
if ($maxRequests > 0 && $requestCount >= $maxRequests) {
break;
}
} while ($running);
Caddyfile Configuration
{
admin off
frankenphp {
worker /app/bin/worker.php {
num {$FRANKENPHP_NUM_WORKERS:4}
}
}
}
{$SERVER_NAME::8080} {
root * /app/public
encode zstd br gzip
respond /health 200
php_server
log {
output stdout
format console
}
}
Run with Docker
docker run -v $PWD:/app -p 8080:8080 dunglas/frankenphp
Environment Variables
| Variable | Default | Description |
|---|---|---|
BEAR_CONTEXT |
prod-hal-app | BEAR.Sunday context |
MAX_REQUESTS |
1000 | Requests before worker restart |
SERVER_NAME |
:8080 | Listen address |
FRANKENPHP_NUM_WORKERS |
4 | Number of worker processes |
Memory Management
- Workers automatically restart after
MAX_REQUESTSto prevent memory leaks gc_collect_cycles()runs after each request- Set
MAX_REQUESTS=0for unlimited requests (development only)
Production Deployment
For production deployments, each server directory in bear-sunday-servers includes:
Dockerfile- Optimized production builddocker-compose.prod.yml- Production configuration- Health check endpoints
- OPcache optimization
Example production deployment:
cd swoole # or roadrunner, frankenphp
docker compose -f docker-compose.prod.yml up -d
Benchmarking
See BEAR.HelloworldBenchmark for benchmark comparisons.
Related
- Parallel Resource Execution - Parallel execution of
#[Embed]resources with BEAR.Async