yangusik / laravel-spawn
Laravel adapter for PHP TrueAsync — async HTTP server with coroutine-per-request isolation
Requires
- php: ^8.6
- ext-pcntl: *
- ext-pdo: *
- laravel/framework: ^12.0 || ^13.0
Requires (Dev)
- barryvdh/laravel-debugbar: ^4.2
- inertiajs/inertia-laravel: ^3.0
- laravel/socialite: ^5.26
- laravel/telescope: ^5.19
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.0
- spatie/laravel-permission: ^7.2
This package is auto-updated.
Last update: 2026-06-02 07:53:50 UTC
README
Laravel adapter for PHP TrueAsync — a PHP fork with a native coroutine scheduler and async I/O. Think Laravel Octane, but instead of Swoole or RoadRunner the runtime is TrueAsync.
One worker. Many requests. Zero threads. Each HTTP request runs in its own coroutine with isolated state — no shared memory, no leaks between requests.
How it works
- Each request = a separate coroutine with its own
Scope - Request-scoped services (
auth,session,cookie) are isolated viacoroutine_context()andrequest_context()(if use True Async Server) - PDO Pool transparently gives each coroutine its own database connection and returns it when the coroutine ends
- No container cloning — isolation is handled at the coroutine level, not by copying the entire app
Requirements
- PHP TrueAsync fork 8.6+
- Laravel 12+
- For FrankenPHP mode:
trueasync/php-true-async:latest-frankenphpDocker image
Installation
composer require yangusik/laravel-spawn
Via git repository:
"repositories": [ { "type": "vcs", "url": "https://github.com/yangusik/laravel-spawn" } ], "require": { "yangusik/laravel-spawn": "dev-master" }
Via local path:
"repositories": [ { "type": "path", "url": "../laravel-true-async" } ], "require": { "yangusik/laravel-spawn": "*" }
Then run composer update.
The service provider is auto-discovered by Laravel.
Replace the Application class in bootstrap/app.php:
- $app = new Illuminate\Foundation\Application( + $app = new Spawn\Laravel\Foundation\AsyncApplication( $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__) );
This is required for per-coroutine isolation of auth, session, and request. Without it the service adapters register correctly but state isolation does not work.
Publish the config:
php artisan vendor:publish --tag=async-config
Servers
True Async Server
Production-ready adapter using True Async Server in async worker mode.
Check config/async.php!
php artisan async:serve --host=0.0.0.0 --port=8080 --workers=1
Dev server
Simple TCP socket server for local development. Analogous to php artisan serve.
php artisan async:dev --host=0.0.0.0 --port=8080
FrankenPHP
Production-ready adapter using FrankenPHP in async worker mode.
Requires the trueasync/php-true-async:latest-frankenphp Docker image.
php artisan async:franken --host=0.0.0.0 --port=8080 --workers=1 --buffer=1
Docker quick start
TrueAsyncServer (better)
services: app: image: trueasync/php-true-async:latest working_dir: /app command: php artisan async:serve # check config/async.php! ports: - "8080:8080" volumes: - .:/app environment: APP_ENV: local DB_CONNECTION: pgsql DB_HOST: db DB_PORT: 5432 DB_DATABASE: laravel DB_USERNAME: laravel DB_PASSWORD: secret
Dev server (no FrankenPHP required)
services: app: image: trueasync/php-true-async:latest working_dir: /app command: php artisan async:dev --host=0.0.0.0 --port=8080 ports: - "8080:8080" volumes: - .:/app environment: APP_ENV: local DB_CONNECTION: pgsql DB_HOST: db DB_PORT: 5432 DB_DATABASE: laravel DB_USERNAME: laravel DB_PASSWORD: secret
FrankenPHP
services: app: image: trueasync/php-true-async:latest-frankenphp working_dir: /app command: php artisan async:franken --host=0.0.0.0 --port=8080 --workers=1 --buffer=1 ports: - "8080:8080" volumes: - .:/app environment: APP_ENV: local DB_CONNECTION: pgsql DB_HOST: db DB_PORT: 5432 DB_DATABASE: laravel DB_USERNAME: laravel DB_PASSWORD: secret
Configuration
config/async.php:
If you use TrueAsyncServer, pls read docs: Configuration
return [ 'db_pool' => [ 'enabled' => env('ASYNC_DB_POOL_ENABLED', true), 'min' => env('ASYNC_DB_POOL_MIN', 2), 'max' => env('ASYNC_DB_POOL_MAX', 10), 'healthcheck_interval' => env('ASYNC_DB_POOL_HEALTHCHECK', 30), ], /* |-------------------------------------------------------------------------- | Async Server |-------------------------------------------------------------------------- | | Configuration for the TrueAsync HTTP server. The server can listen | on multiple interfaces and protocols simultaneously. | */ 'server' => [ /* |-------------------------------------------------------------------------- | Listeners |-------------------------------------------------------------------------- | | Define the TCP interfaces the server should bind to. Each listener | can use a specific HTTP protocol version and optional TLS. | | Available protocols: auto, http1, http2, http3 | */ 'listeners' => [ [ 'host' => env('ASYNC_HOST', '0.0.0.0'), 'port' => (int) env('ASYNC_PORT', 8080), 'tls' => (bool) env('ASYNC_TLS', false), 'protocol' => env('ASYNC_PROTOCOL', 'auto'), // auto, http1, http2, http3 ], ], /* |-------------------------------------------------------------------------- | Workers |-------------------------------------------------------------------------- | | Number of worker threads for the multi-threaded server command | (async:workers). 0 means auto-detect based on CPU core count. | */ 'workers' => (int) env('ASYNC_WORKERS', 0), /* |-------------------------------------------------------------------------- | TLS Certificates |-------------------------------------------------------------------------- | | Absolute paths to the TLS certificate and private key. Used when | at least one listener has 'tls' => true. | */ 'tls_cert' => env('ASYNC_TLS_CERT', '/certs/server.crt'), 'tls_key' => env('ASYNC_TLS_KEY', '/certs/server.key'), /* |-------------------------------------------------------------------------- | Socket & HTTP Settings |-------------------------------------------------------------------------- */ 'backlog' => (int) env('ASYNC_BACKLOG', 2048), 'compression' => (bool) env('ASYNC_COMPRESSION', true), 'max_body_size' => (int) env('ASYNC_MAX_BODY_SIZE', 32 * 1024 * 1024), 'read_timeout' => (int) env('ASYNC_READ_TIMEOUT', 60), 'write_timeout' => (int) env('ASYNC_WRITE_TIMEOUT', 60), /* |-------------------------------------------------------------------------- | Static File Handlers |-------------------------------------------------------------------------- | | Map URL prefixes to local directories for direct static file serving | bypassing the Laravel kernel. | | Example: | [ | 'prefix' => '/assets/', | 'root' => public_path('assets'), | 'etag' => true, | 'precompressed' => ['br', 'gzip'], | ] | */ 'static_handlers' => [], ], ];
Benchmarks (Obsolete use TrueAsyncServer)
Check results in HttpArena
Load: 840 req/s /hello + 360 req/s /test = 1 200 req/s total · constant-arrival-rate · 30s · 12 workers each · WSL2 (Linux 6.6 on Windows)
| Metric | PHP-FPM (12w) | Octane Swoole (12w) | TrueAsync-Franken (12w) |
|---|---|---|---|
| Target rate | 1 200 req/s | 1 200 req/s | 1 200 req/s |
| Actual throughput | ~200 req/s | ~752 req/s | ~1 118 req/s |
| Dropped iterations | ~28 000 | ~5 000 | 20 |
| Avg latency | ~4 000ms | ~880ms | 13ms |
| p95 latency | ~5 000ms | 2 320ms | 21ms |
| p95 < 200ms | ✗ | ✗ | ✓ |
| Failed requests | 0% | 0% | 0% |
| DB connections (peak) | — | — | 120 |
Why TrueAsync wins on DB-bound load
| PHP-FPM | Octane Swoole | TrueAsync-Franken | |
|---|---|---|---|
| Request model | Process per request | 1 process = 1 request at a time | 1 worker = N coroutines |
| DB I/O | Blocking (new conn each req) | Blocking (PDO synchronous) | Non-blocking (coroutine yield) |
| Memory model | Stateless | Long-lived process | Long-lived process + coroutine context isolation |
| App bootstrap | Every request | Once per worker | Once per worker |
Swoole keeps the app in memory (avoids bootstrap cost) but PDO is still synchronous — a worker blocked on a DB call cannot accept another request. TrueAsync yields the coroutine on every DB call, so one worker handles hundreds of concurrent DB-bound requests without blocking.
Notes
- Each adapter has its own PostgreSQL instance on a separate port to avoid interference
APP_DEBUG=falsein all setups for fair comparison- OPcache enabled in PHP-FPM
max_connections=500in all PostgreSQL instances- Absolute numbers will be higher on bare metal (benchmarks run on WSL2)
Full benchmark: ta_benchmark
Raw PHP — TrueAsync vs Swoole (no framework, no I/O)
On pure CPU-bound workloads both servers cap at the same throughput (~10k req/s). With optimal Swoole config (ZTS, 16 reactor threads) Swoole is ~1.6x faster on P95 latency due to FrankenPHP's Go↔PHP boundary overhead (futex synchronization). On I/O-bound workloads this overhead is negligible.
Sessions
Database sessions (built-in fix)
The package automatically replaces Laravel's DatabaseSessionHandler with an async-safe version that uses upsert instead of INSERT + catch + UPDATE.
In a standard async server the HTTP response is sent before kernel->terminate() writes the session. If the client immediately sends the next request with the same cookie, two coroutines can race to INSERT the same session ID — causing duplicate-key warnings in the stock handler. The upsert is atomic, so this race is impossible regardless of concurrency.
No configuration needed. Works transparently when SESSION_DRIVER=database.
Redis sessions (recommended for production)
For high-concurrency workloads Redis sessions have lower overhead than database sessions and avoid any persistence race entirely:
SESSION_DRIVER=redis REDIS_HOST=127.0.0.1
License
MIT