Laravel Octane Deployment
AutoCom uses Laravel Octane with FrankenPHP for production API serving. This replaces the traditional PHP-FPM + Nginx stack with a single high-performance binary that keeps the application in memory.
Performance Impact
| Metric (500 concurrent users) | PHP-FPM | Octane | Improvement |
|---|---|---|---|
| Throughput | 27 req/s | 456 req/s | 17x |
| p50 response | 343ms | 122ms | 2.8x |
| p95 response | 2.44s | 1.43s | 1.7x |
| Bootstrap overhead per request | 30-50ms | 0ms | Eliminated |
How It Works
PHP-FPM boots Laravel from scratch on every request — load config, register 22 modules, parse routes, initialize service containers. That's 30-50ms before your code runs.
Octane boots Laravel once, keeps it in memory, and reuses everything. Each request runs only your controller logic (~1-5ms).
Dockerfile
FROM dunglas/frankenphp:latest
RUN install-php-extensions pdo_pgsql pgsql redis pcntl sockets intl zip opcache bcmath
COPY backend/ /app/
COPY modules/ /app/modules/
RUN composer install --no-dev --no-scripts --ignore-platform-reqs
CMD ["php", "artisan", "octane:start", "--server=frankenphp", "--host=0.0.0.0", "--port=8000", "--workers=auto", "--max-requests=1000"]
Key flags:
--workers=auto— scales to CPU cores--max-requests=1000— restarts workers after 1000 requests to prevent memory leaks
Multi-Tenant Safety
Since Octane keeps the app in memory, tenant state can leak between requests if not handled properly.
What Stancl Tenancy handles automatically
- Database connection switching per request
- Cache prefix per tenant
- Storage disk per tenant
- Queue tenant context
What you must watch for
Static properties in service providers:
// DANGEROUS — persists between requests
class MyService {
private static $tenantData = null;
public function getData() {
if (self::$tenantData === null) {
self::$tenantData = DB::table('settings')->get(); // Tenant A's data!
}
return self::$tenantData; // Returns Tenant A's data for Tenant B!
}
}
Fix: Use request-scoped singletons or flush in Octane's OperationTerminated listener:
// In OctaneServiceProvider or AppServiceProvider
Octane::on('RequestTerminated', function () {
// Flush any tenant-specific singletons
app()->forgetInstance(MyService::class);
});
Octane Listeners Registered
AutoCom registers these flush handlers automatically:
| Event | What it flushes |
|---|---|
RequestTerminated |
Tenant context, module singletons, AI manager |
TaskTerminated |
Same as above (for Octane tasks) |
TickTerminated |
Cache statistics counters |
Database: Read/Write Splitting
With CloudNativePG (1 primary + 2 replicas), reads are distributed across replicas:
// config/database.php
'pgsql' => [
'read' => ['host' => env('DB_READ_HOST')], // autocom-db-ro (replicas)
'write' => ['host' => env('DB_HOST')], // autocom-db-rw (primary)
'sticky' => true, // After a write, reads use primary for that request
],
Replication Lag Handling
Async replication means writes take 10-100ms to appear on replicas.
sticky => true handles same-request reads (user creates order → same request reads it back = works).
Cross-request reads (create order → redirect → load order page) can hit stale replica. For critical read-after-write flows, force the primary:
// Force read from primary when stale data is unacceptable
$order = DB::connection('pgsql')->useWritePdo()->table('orders')->find($id);
// Or use the ReadWriteSafe middleware on specific routes
Route::get('/orders/{id}', OrderController::class)->middleware('read.primary');
Monitoring
Memory
Watch Octane worker memory — if it grows over time, there's a leak:
kubectl top pods -n autocom -l app=api
Octane restarts workers after --max-requests=1000 to bound memory growth.
Replication Lag
CloudNativePG exposes replication lag:
kubectl get cluster autocom-db -n autocom -o jsonpath='{.status.replicaCluster}'
Worker Status
kubectl exec -n autocom deployment/api -- php artisan octane:status
Trade-offs
| Gain | Trade-off | Mitigation |
|---|---|---|
| 17x throughput | Memory leaks accumulate | --max-requests=1000 |
| Zero bootstrap overhead | Static state persists between requests | Flush listeners + Octane-safe patterns |
| Single binary (no Nginx) | Less battle-tested than Nginx+FPM | Nginx proxy in front for CORS/caching |
| Read/write split (3x DB throughput) | Replication lag on reads | sticky => true + force primary for critical reads |
| Auto-failover | Split-brain risk (rare) | CloudNativePG handles with fencing |
| 3x DB capacity | 3x resource usage | Right-size replicas |