Por Marcelo Jean de Almeida Pena Especialista em Desenvolvimento e Ecossistema Web
Você migrou para Laravel Octane porque leu que é 5x, 10x, até 50x mais rápido que FPM. Seu dashboard agora renderiza em 30 ms ao invés de 300ms. Seus usuários comemoram. Você dormiu bem na primeira noite.
Na segunda semana, seu servidor começa a ficar lento. Não é gradual — é um penhasco. Requisições que respondiam em 40 ms agora levam 2 segundos. Você reinicia o Octane. Rápido de novo. Uma hora depois, colapso novamente.
Você não tem um problema de performance. Você tem um vazamento de memória.
Aqui está a verdade que ninguém enfatiza o suficiente: Octane rodando sob Swoole é um processo persistente que nunca “morre” entre requisições. Ao contrário do FPM, que spawna um novo worker para cada requisição e descarta toda a memória alocada depois, Swoole mantém a mesma instância de PHP viva durante horas. Cada requisição que vaza 1 MB de memória não-liberada é 1 MB perdido permanentemente até você reiniciar o servidor.
Este artigo não é sobre como usar Octane. É sobre como auditar sua aplicação para identificar exatamente onde o sangramento de memória está acontecendo, entender por que concorrentes com FPM dormem tranquilos enquanto você sua frio às 3 da manhã, e implementar monitoramento que realmente funciona em produção.
O paradigma de memória: por que seu código PHP tradicional falha em Octane
A ilusão da “limpeza automática” do FPM
Em um cenário FPM típico, sua aplicação segue este ciclo:
- Worker nasce → Carrega o autoloader, bootstrap da aplicação
- Requisição chega → Código executa
- Resposta é enviada → Worker morre
- Memória é devolvida ao SO → Limpa automaticamente
Esse modelo é uma bênção mascarada. Mesmo que seu código seja um desastre de vazamentos de memória—variáveis globais não-zeradas, closures capturando $this, listeners de eventos acumulando em arrays estáticos—tudo desaparece em 300ms.
O mundo persistente do Swoole: uma aplicação que nunca “respira”
Quando você inicia php artisan octane:start --server=swoole, você está criando processos de trabalho que permanecem vivos indefinidamente. Ao invés do ciclo “nascer → executar → morrer”, você tem:
- Worker nasce uma vez → Carrega a aplicação
- Requisição 1 chega → Código executa → Memória é alocada
- Requisição 2 chega → Mesmo worker, mesma memória → Se a req1 não limpou tudo, os dados permanecem
- Requisição 3 chega → Mesmo problema, mas agora temos 2x vazamento
- Após 1000 requisições → O servidor consumiu 1-2GB de RAM que nunca será devolvido
O problema não é que Octane seja “inseguro”. O problema é que Octane expõe cada ineficiência de memória que seu código FPM disfarçava.
Antes de escalar seu Octane, entenda como o Auto-scaling reativo pode chegar tarde demais se sua memória já estiver saturada
Onde o vazamento acontece: os 6 padrões mais silenciosos
Padrão 1: Listeners de eventos com closures que capturam $this
// Model.php - Esse é um dos piores culpritos
class Order extends Model
{
protected static function boot()
{
parent::boot();
static::created(function ($order) {
// ⚠️ PROBLEMA: $this (a instância do Model) é capturada pelo listener
// Em FPM, isso desaparece quando a requisição morre
// Em Octane, o closure permanece vivo, referenciando a instância criada
$order->notifyCustomer();
});
}
}
Por que é um problema em Octane: Cada Model criado em uma requisição fica na memória através do listener. A closure captura referências que impedem garbage collection. Após 1000 Orders criadas, você tem 1000 instâncias mortas flotando na RAM.
Verdade técnica: O Swoole não tem ciclo de garbage collection entre requisições (diferente de alguns servidores Node.js). A responsabilidade é 100% sua.
Padrão 2: Arrays estáticos que acumulam sem nunca limpar
// Cache implementado de forma amadora
class RequestCache
{
private static array $cache = [];
public static function set($key, $value)
{
self::$cache[$key] = $value; // Cresce infinitamente
}
}
// Em uma rota
RequestCache::set('user_' . $user->id, $user);
Em produção: Após 10.000 requisições de usuários diferentes, você tem 10.000 usuários inteiros gravados em memória.
Padrão 3: builders do Eloquent que não são resetados
class UserRepository
{
private $query; // ⚠️ Mantém estado entre requisições!
public function __construct()
{
$this->query = User::query();
}
public function findActive()
{
return $this->query->where('active', true)->get();
}
}
// Em seu container, isso é um singleton
app()->singleton(UserRepository::class);
O que acontece: Req 1 tem um builder, Req 2 ainda tem resquícios da Req 1, Octane acumula bindings e estados parciais.
Padrão 4: Listeners de banco de dados não despachados
// Seu middleware registra um listener de query para logging
DB::listen(function ($query) {
Log::debug($query->sql);
});
Cada middleware que roda (e não remove o listener) deixa uma cópia daquele callback na memória.
Padrão 5: Middleware que manipula $request->attributes de forma Permanente
class AuthMiddleware
{
public function handle(Request $request, $next)
{
$user = auth()->user();
$request->attributes->put('user_data', $user); // Fica na requisição
return $next($request);
}
}
Se a Request não é descartada corretamente entre ciclos, esses attributes ocupam espaço.
Padrão 6: Conexões de Banco de Dados Abertas Implicitamente
// Seu code abre uma conexão PDO dentro de um comando artisan
// Se o comando roda múltiplas vezes, você acumula conexões
$pdo = new PDO('mysql:host=...', 'user', 'pass');
Em Octane, se isso rodar dentro de um job processado pelo worker, a conexão pode permanecer aberta.
Auditoria: montando o sistema de detecção de Memory Leaks
Aqui é onde a maioria dos artigos genéricos falha. Vou te mostrar exatamente como capturar dados de vazamento em tempo real.
Passo 1 – Instrumentação básica com Memory Profiling
Crie um middleware que registra o consumo de memória por requisição:
// app/Http/Middleware/MemoryProfiler.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class MemoryProfiler
{
private static ?int $startMemory = null;
private static ?int $peakMemory = null;
public function handle(Request $request, Closure $next)
{
self::$startMemory = memory_get_usage(true);
$response = $next($request);
$endMemory = memory_get_usage(true);
$diff = $endMemory - self::$startMemory;
// Log somente se houver vazamento
if ($diff > 100 * 1024) { // > 100KB
Log::warning('Memory leak detected', [
'path' => $request->path(),
'method' => $request->method(),
'memory_allocated_kb' => $diff / 1024,
'peak_memory_mb' => memory_get_peak_usage(true) / (1024 * 1024),
'timestamp' => now(),
]);
}
return $response;
}
}
Registre no kernel:
protected $middleware = [
// ... outros middlewares
\App\Http\Middleware\MemoryProfiler::class,
];
O que isso vai revelar: Quais rotas estão comendo memória.
Passo 2 – Profiling granular com Debug Bar customizado
Para Octane, o Laravel Debugbar tradicional é problemático (ele próprio acumula dados). Use uma abordagem mais leve:
// app/Services/MemoryAuditService.php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
class MemoryAuditService
{
public static function checkpointMemory(string $label): void
{
$current = memory_get_usage(true) / (1024 * 1024); // MB
$peak = memory_get_peak_usage(true) / (1024 * 1024);
$data = Cache::remember(
"memory_audit_{$label}",
60,
fn () => []
);
$data[] = [
'timestamp' => microtime(true),
'current_mb' => $current,
'peak_mb' => $peak,
];
Cache::put("memory_audit_{$label}", $data, 60);
}
public static function getReport(string $label): array
{
return Cache::get("memory_audit_{$label}", []);
}
}
Use assim:
class OrderController
{
public function create(Request $request)
{
MemoryAuditService::checkpointMemory('before_order_create');
$order = Order::create($request->validated());
MemoryAuditService::checkpointMemory('after_order_create');
MemoryAuditService::checkpointMemory('after_notification');
return response()->json($order);
}
}
Depois acesse: /debug/memory-audit?label=after_order_create para ver o crescimento.
Passo 3 – Capturando listeners fantasmas com Reflection
Este é o culpado número 1 em apps legadas. Vamos exposá-lo:
// app/Commands/AuditListenersCommand.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Events\Dispatcher;
use ReflectionFunction;
class AuditListenersCommand extends Command
{
protected $signature = 'audit:listeners';
protected $description = 'Detect accumulated listeners that might cause memory leaks';
public function handle(Dispatcher $dispatcher)
{
// Acessa o array privado de listeners
$listeners = (fn() => $this->listeners)->bindTo($dispatcher, $dispatcher)();
$problematic = [];
foreach ($listeners as $event => $callbacks) {
if (count($callbacks) > 5) {
$problematic[$event] = count($callbacks);
}
foreach ($callbacks as $callback) {
if (is_array($callback)) {
[$object, $method] = $callback;
$this->line("Event: <fg=yellow>$event</> | Listener: " . get_class($object) . "@$method");
} elseif ($callback instanceof \Closure) {
$reflection = new ReflectionFunction($callback);
$uses = $reflection->getStaticVariables();
if (count($uses) > 0) {
$this->line("<fg=red>⚠️ CLOSURE CAPTURES VARIABLES:</> $event");
foreach ($uses as $varName => $value) {
$this->line(" - \$$varName: " . get_class($value));
}
}
}
}
}
if (!empty($problematic)) {
$this->warn("\n⚠️ EVENTS WITH EXCESSIVE LISTENERS:");
foreach ($problematic as $event => $count) {
$this->line(" <fg=red>$event</>: $count listeners");
}
}
}
}
Execute: php artisan audit:listeners
Isso vai expor exatamente quais eventos têm closures capturando $this.
Passo 4 – Monitoramento em tempo real com Systemd + Script
Para produção, você precisa de alertas que não dependam da sua app:
#!/bin/bash
# /home/deploy/monitor-octane.sh
OCTANE_PID=$(pgrep -f "php artisan octane:start")
MEMORY_THRESHOLD_MB=1000 # Alerta se > 1GB
while true; do
if [ ! -z "$OCTANE_PID" ]; then
MEMORY=$(ps -p $OCTANE_PID -o rss= | awk '{print int($1/1024)}')
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$TIMESTAMP] Octane Memory: ${MEMORY}MB"
if [ $MEMORY -gt $MEMORY_THRESHOLD_MB ]; then
echo "ALERT: Memory threshold exceeded!" | mail -s "Octane Memory Leak Alert" ops@company.com
# Reinicia gracefully
kill -TERM $OCTANE_PID
sleep 5
cd /path/to/app && php artisan octane:start --server=swoole &
fi
fi
sleep 30
done
Coloque em systemd:
# /etc/systemd/system/octane-monitor.service
[Unit]
Description=Octane Memory Monitor
After=network.target
[Service]
Type=simple
User=www-data
ExecStart=/home/deploy/monitor-octane.sh
Restart=on-failure
[Install]
WantedBy=multi-user.target
Inicie: systemctl start octane-monitor
Otimização: Patterns que funcionam em Swoole
Não é suficiente identificar o problema. Você precisa reescrever seu código.
Padrão 1 – Listeners com Cleanup explícito
| RUIM | BOM |
|---|---|
class Order extends Model { protected static function boot() { parent::boot(); static::created(function ($order) { $order->notifyCustomer(); }); } } | |
Razão: Listeners como classes são instanciados e descartados por requisição. Closures em boot() vivem eternamente.
Padrão 2 – Repositórios sem estado (Stateless)
| RUIM | BOM |
|---|---|
class UserRepository { private $query; public function __construct() { $this->query = User::query(); } } | class UserRepository { public function getActive() { return User ::where('active', true) ->get(); } public function findById($id) { return User::find($id); } } |
Razão: Cada método cria um novo builder. Nenhum estado persiste.
Padrão 3 – Cache em escopo requisição (Request-Scoped)
// app/Services/RequestScopedCache.php
namespace App\Services;
class RequestScopedCache
{
private static array $data = [];
public static function set($key, $value)
{
self::$data[$key] = $value;
}
public static function get($key, $default = null)
{
return self::$data[$key] ?? $default;
}
public static function flush()
{
self::$data = [];
}
}
// Em app/Http/Middleware/FlushRequestCache.php
class FlushRequestCache
{
public function handle(Request $request, Closure $next)
{
$response = $next($request);
RequestScopedCache::flush();
return $response;
}
}
Isso garante que o cache vaza apenas durante uma requisição, nunca entre elas.
Padrão 4 – Lazy loading de observers
| RUIM | BOM |
|---|---|
class User extends Model { protected static function boot() { parent::boot(); static::observe( UserObserver::class ); } } | class UserObserver { public function created($user) { dispatch( new SendWelcomeEmail( $user ) ); } } |
Razão: Jobs são enfileirados e processados fora do ciclo de requisição. Nenhuma memória acumulada no worker.
Padrão 5 – Cleanup de conexões explícito
// app/Services/ExternalApiClient.php
class ExternalApiClient
{
private $client;
public function __construct()
{
$this->client = new HttpClient(
['timeout' => 10]
);
}
public function fetch($url)
{
try {
return $this->client->get($url);
} finally {
// Force cleanup
$this->client = null;
gc_collect_cycles();
}
}
}
// Ou faça um middleware que limpa resources
class CleanupResourcesMiddleware
{
public function handle(Request $request, Closure $next)
{
try {
return $next($request);
} finally {
// Force garbage collection
gc_collect_cycles();
// Feche conexões explicilmente
\DB::disconnect();
}
}
}
Cenário de uso A vs. B: quando Octane vale a pena (e quando não)
Cenário A – API Laravel com 1000+ Requisições/Minuto e dados sensíveis a latência
Perfil:
- SPA React/Vue comunicando constantemente
- Upload de arquivos
- Cache frequente
- Sem listeners complexos
Octane Vale? SIM, MUITO
Por quê:
- Latência reduz drasticamente (300ms → 30ms)
- A taxa de requisição amortiza overhead de monitoramento
- Listeners simples (enfileirados) não causam problema
Setup Recomendado:
// config/octane.php
return [
'server' => 'swoole',
'listeners' => [
0 => ['*'],
],
'workers' => env('OCTANE_WORKERS', cpu_count()),
'max_requests' => 500, // Reinicia a cada 500 reqs
];
A chave: max_requests baixo reduz vazamento cumulativo.
Cenário B – Sistema de relatórios + processamento em lote com Models legados
Perfil:
- 200 requisições/dia
- Models com 10+ observers cada um
- Cache complexo
- Controllers que acumulam dados em memoria
Octane Vale? TALVEZ NÃO
Por quê:
- Ganho de latência é 100ms em requisições que levam 2s
- Necessidade de refactor profundo
- Risco vs. retorno baixo
Alternativa:
# Fica com FPM + Nginx cache + Redis
# Mais seguro, mais fácil de debugar
O teste do especialista: sua aplicação está pronta para Octane?
Execute este checklist com sua app:
- Rodar
php artisan audit:listeners→ Se há mais de 3 listeners por evento, RISCO - Procurar por
static::created()eboot()→ Cada closure = vazamento potencial - Rodar
php artisan config:cache→ Se houver erros com listeners, NÃO está pronto - Testar com 10.000 requisições artificiais:
ab -n 10000 -c 50 http://your-app.local/api/endpointMonitorarps aux | grep octane→ Se memória cresce acima de 2x, tem vazamento - Procurar por
new PDO()ou conexões não-pooled → Cause vazamento garantido
Se falhar em 3+ pontos, Octane não é a solução agora.
Infraestrutura de produção: Deploying Octane com segurança
Systemd Unit com health checks
# /etc/systemd/system/laravel-octane.service
[Unit]
Description=Laravel Octane Server
After=network.target
Wants=laravel-octane-monitor.service
[Service]
Type=notify
User=www-data
WorkingDirectory=/var/www/app
ExecStart=/usr/bin/php /var/www/app/artisan octane:start \
--server=swoole \
--workers=4 \
--max-requests=1000 \
--port=8000
ExecStartPost=/bin/bash -c 'while true; do \
MEMORY=$(ps -p $MAINPID -o rss= | awk "{print int($1/1024)}"); \
if [ $MEMORY -gt 1500 ]; then \
systemctl restart laravel-octane; \
fi; \
sleep 60; \
done &'
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
Nginx como Reverse Proxy com Failover
upstream octane_upstream {
server 127.0.0.1:8000 max_fails=3 fail_timeout=30s;
server 127.0.0.1:8001 backup;
}
server {
listen 80;
server_name api.company.com;
location / {
proxy_pass http://octane_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 60s;
}
location /health {
access_log off;
return 200 "OK";
}
}
Execute múltiplas instâncias do Octane:
php artisan octane:start --server=swoole --port=8000 &
php artisan octane:start --server=swoole --port=8001 &
Se uma vaza memória e reinicia, o Nginx já roteou para a outra.
Conclusão: Laravel Octane é um superpoder que exige responsabilidade
Laravel Octane não é “FPM mais rápido”. É um modelo de arquitetura completamente diferente que expõe cada ineficiência do seu código.
A verdade oculta que mencionei no início é real: você não pode simplesmente upgradar para Octane e esperar que funcione. Você precisa:
- Auditar seu código com as ferramentas que mostrei
- Refatorar listeners, observers, caches
- Monitorar continuamente em produção
- Reiniciar gracefully quando necessário
Se você fizer isso, Octane entrega velocidade que é genuinamente transformadora. Se não fizer, seu servidor cairá às 3 da manhã e você descobrirá por acaso no seu monitor de infraestrutura.
A escolha é sua. Mas agora você tem o mapa.





