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