Como Implementar Processamento Assíncrono de Relatórios Pesados em PHP e MySQL
Um dos problemas mais comuns em aplicações web maduras é o estouro de memória (Fatal error: Allowed memory size exhausted) e o estouro de tempo limite (HTTP 504 Gateway Timeout) ao tentar gerar relatórios em PDF ou CSV com milhares de registros dentro de uma requisição HTTP tradicional.
Quando o usuário clica em “Exportar Relatório”, o servidor web tenta buscar todos os dados no banco MySQL, formatá-los e gerar o arquivo de uma só vez, mantendo a conexão aberta. Isso paralisa a experiência do usuário e consome recursos do servidor de forma destrutiva.
A solução definitiva para este problema é desacoplar a requisição HTTP da execução da tarefa pesada, implementando uma arquitetura de processamento assíncrono em background nativa em PHP e MySQL.
A Arquitetura do Processamento em Background
Em vez de gerar o arquivo imediatamente, a requisição HTTP apenas registra a intenção de gerar o relatório na base de dados e devolve uma resposta instantânea ao usuário.
1. Modelagem da Tabela de Trabalhos (Jobs)
Crie uma tabela dedicada no MySQL para gerenciar a fila de processamento:
CREATE TABLE relatorios_queue (
id INT AUTO_INCREMENT PRIMARY KEY,
usuario_id INT NOT NULL,
status ENUM('pendente', 'processando', 'concluido', 'falha') DEFAULT 'pendente',
parametros JSON NULL,
arquivo_path VARCHAR(255) NULL,
erro_mensagem TEXT NULL,
criado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
atualizado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_status (status)
) ENGINE=InnoDB;
2. A Requisição HTTP (Rápida e Não-Bloqueante)
No endpoint acessado pelo usuário, você insere um registro com o status pendente e retorna o ID da solicitação imediatamente:
// endpoint_exportar.php
$usuarioId = $_SESSION['user_id'];
$filtros = json_encode(['data_inicio' => '2023-01-01', 'data_fim' => '2023-12-31']);
$stmt = $pdo->prepare("INSERT INTO relatorios_queue (usuario_id, parametros) VALUES (?, ?)");
$stmt->execute([$usuarioId, $filtros]);
echo json_encode([
'sucesso' => true,
'mensagem' => 'Seu relatório está sendo gerado. Você será notificado assim que estiver pronto.',
'job_id' => $pdo->lastInsertId()
]);
O Worker CLI e a Gestão de Memória no PHP
Para processar o relatório sem travar a interface web, utilizamos um script PHP executado via linha de comando (CLI). O segredo para não esgotar a memória RAM no MySQL é utilizar leitura paginada ou cursores do PDO (PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false).
Em sistemas PHP que desenvolvo para cenários de alta carga, aplico o processamento em lotes (chunking) para garantir um consumo de memória constante, independentemente se o banco possui 10 mil ou 10 milhões de registros:
// worker.php (Executado via CLI/Cron)
$stmt = $pdo->prepare("SELECT * FROM relatorios_queue WHERE status = 'pendente' LIMIT 1 FOR UPDATE");
$stmt->execute();
$job = $stmt->fetch();
if ($job) {
// Transição atômica de status
$pdo->prepare("UPDATE relatorios_queue SET status = 'processando' WHERE id = ?")->execute([$job['id']]);
try {
$filePath = '/var/www/exports/relatorio_' . $job['id'] . '.csv';
$file = fopen($filePath, 'w');
// Escreve o cabeçalho CSV
fputcsv($file, ['ID', 'Cliente', 'Valor', 'Data']);
// Leitura paginada com OFFSET para manter baixo uso de memória
$limit = 1000;
$offset = 0;
do {
$query = $pdo->prepare("SELECT id, cliente, valor, data FROM vendas LIMIT :limit OFFSET :offset");
$query->bindValue(':limit', $limit, PDO::PARAM_INT);
$query->bindValue(':offset', $offset, PDO::PARAM_INT);
$query->execute();
$rows = $query->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
fputcsv($file, $row);
}
$offset += $limit;
} while (count($rows) === $limit);
fclose($file);
// Atualiza job como concluído
$update = $pdo->prepare("UPDATE relatorios_queue SET status = 'concluido', arquivo_path = ? WHERE id = ?");
$update->execute([$filePath, $job['id']]);
} catch (Exception $e) {
$update = $pdo->prepare("UPDATE relatorios_queue SET status = 'falha', erro_mensagem = ? WHERE id = ?");
$update->execute([$e->getMessage(), $job['id']]);
}
}
Fluxo Recomendado de Execução
- Solicitação: O usuário solicita o relatório; o backend grava o trabalho no MySQL com status
pendentee responde em menos de 200ms. - Agendamento: Um Cron Job no servidor executa o script
worker.phpa cada minuto ou um daemon (Supervisor) mantém o worker ativo em segundo plano. - Processamento em Lotes: O script busca o job pendente, consome o banco de dados em pequenos pedaços (chunks) e grava direto no disco em formato CSV/PDF.
- Finalização: O status é alterado para
concluidoe a URL do arquivo é disponibilizada no painel do usuário para download.
Precisa Otimizar a Performance da sua Aplicação PHP?
Se o seu sistema atual sofre com lentidão, quedas de servidor ao gerar exportações ou problemas de consumo excessivo de memória no MySQL, a reestruturação da arquitetura para modelos assíncronos é o caminho mais seguro e escalável.
Como especialista em desenvolvimento e arquitetura PHP, ajudo empresas a eliminar gargalos de performance e construir sistemas estáveis. Entre em contato para agendarmos uma consultoria técnica sobre seu projeto.


