Лекция
асинхронность на backend — это не просто “модно и быстро”, а способ разделить время ответа пользователю и время выполнения задачи. В классическом синхронном сценарии сервер обрабатывает запрос полностью и только потом отправляет ответ. Это означает, что пользователь ждет завершения всей логики, даже если большая ее часть не влияет на сам ответ.
Современные backend-приложения, включая проекты на Laravel, используют асинхронные подходы, чтобы:
Асинхронность — это не один инструмент, а набор архитектурных решений: очереди, фоновые процессы, стриминг ответов, события и отложенные действия после ответа.

Важно сразу убрать распространенное заблуждение:
асинхронность на backend ≠ обязательно параллельность или event loop как в Node.js.
На практике это означает:
Сервер может:
Пример:
Задача выполняется:
Это самый распространенный вид асинхронности в PHP/Laravel.
Задачи выполняются вне HTTP-запроса:
Это уже настоящая асинхронная архитектура, потому что:
Ответ не ждет завершения всей логики:
Примеры:
Система реагирует на события:
Это уже шаг к более сложной архитектуре (event-driven systems).
Ключевая идея
Асинхронность — это не про “быстрее выполняется”, а про:
пользователь не ждет то, что не обязан ждать
| Классическая очередь | Приоритетная очередь | Quorum очередь | системой с автоскейлингом консумеров (consumer autoscaling) | Streams |
|---|---|---|---|---|
| Пример - Laravel Quare | Пример - доп колонка приоритета | Пример – отказоустойчивые очереди | Пример – Kafka Consumer Groups, Cloudflare Queues | Пример – RabbitMQ Streams |
| Лидер на одном узле | Лидер на одном узле | Лидер + реплики | Нет лидера, распределение по партициям/воркерам | Репликация по своей модели |
| Не масштабируется автоматически |
Сообщения сортируются по приоритету или важности |
Масштабируется за счет реплик | Масштабируется автоматически за счет добавления консумеров | Подходит для больших потоков данных |
| Быстро, но узкое место | Полезно для критичных задач | Более надежно, но дороже | Гибко и эластично, нагрузка распределяется | Высокая пропускная способность |
| FIFO порядок | FIFO с приоритетами (высший приоритет обрабатывается первым) | FIFO с репликацией | FIFO или распределенный порядок (зависит от модели) | Потоковая модель |
| Нет автоматического масштабирования | Полезно для критичных задач | Нет автоматического масштабирования | втоматическое масштабирование консумеров | Подходит для больших нагрузок |
В контексте RabbitMQ термины «лидер» и «узел» относятся к архитектуре кластера и организации очередей:
Узел RabbitMQ — это отдельный экземпляр (инстанс) RabbitMQ, запущенный на сервере или контейнере.
В кластере может быть несколько узлов, которые совместно хранят метаданные (пользователи, виртуальные хосты, политики).
Каждый узел может обслуживать свои очереди, но очередь всегда «привязана» к конкретному узлу.
Лидер (Leader)
Для каждой очереди выбирается лидер очереди — это узел, на котором физически хранится очередь и который отвечает за ее обработку.
Все операции с этой очередью (запись сообщений, чтение консумерами) проходят через лидера.
Если очередь реплицируется (например, quorum queue), то есть несколько копий на разных узлах, но все равно есть один лидер, который принимает решения и обслуживает клиентов.
При сбое лидера кластер может выбрать нового лидера из реплик, чтобы очередь продолжала работать.
Пример
У вас есть кластер из 3 узлов: node1, node2, node3.
Очередь orders создана на node2. → Лидер очереди = node2.
Если очередь quorum, то копии могут быть на node1 и node3, но лидер все равно один (например, node2).
При перегрузке очереди добавление новых узлов не помогает напрямую — очередь остается на своем лидере. Нужно либо увеличить число консумеров, либо использовать архитектуру с несколькими очередями.
Когда асинхронность нужна
Используй ее, если есть:
Когда не нужна
Не стоит усложнять, если:
Асинхронность добавляет:
что реально можно сделать на чистом PHP, где ответ становится “асинхронным” для пользователя, и в чем ограничения CGI/FPM.
Основные варианты:
Сразу вернуть ответ, а работу продолжить после него
Это самый практичный вариант для веба на PHP-FPM: отдать клиенту "accepted" или JSON со статусом, а потом продолжить выполнение кода на сервере. Для FastCGI/FPM для этого есть fastcgi_finish_request(), после которого ответ пользователю уже завершен, а PHP-скрипт еще может выполнять логику дальше.
Пример:
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'status' => 'accepted',
'message' => 'Task started'
]);
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
// После этого пользователь уже получил ответ,
// а сервер может продолжать работу
file_put_contents(__DIR__ . '/log.txt', date('c') . " heavy work started\n", FILE_APPEND);
sleep(5); // имитация тяжелой работы
file_put_contents(__DIR__ . '/log.txt', date('c') . " heavy work finished\n", FILE_APPEND);
Отдать ответ и запустить фоновую задачу отдельным процессом
Это тоже очень популярный путь. PHP-скрипт быстро отвечает, а потом через exec() / shell_exec() / proc_open() запускает отдельный CLI-скрипт в фоне. Это уже не “настоящая асинхронность” внутри одного процесса, а делегирование работы другому процессу.
Пример:
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['status' => 'accepted']);
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
$php = PHP_BINARY;
$script = __DIR__ . '/worker.php';
// Linux/macOS
exec("$php " . escapeshellarg($script) . " > /dev/null 2>&1 &");
worker.php:
file_put_contents(__DIR__ . '/log.txt', date('c') . " worker start\n", FILE_APPEND);
sleep(10);
file_put_contents(__DIR__ . '/log.txt', date('c') . " worker done\n", FILE_APPEND);
Long polling
PHP может держать запрос открытым какое-то время и ждать событие, а потом вернуть ответ. Это не совсем асинхронность на сервере, но для клиента выглядит как “ответ пришел позже сам”. Подходит для чатов, уведомлений, простых live-обновлений.
Схема:
Минусы:
Streaming / chunked response
PHP может отправлять данные частями через echo + flush(). Это удобно, когда нужно не ждать конца работы, а постепенно слать прогресс. Но надо помнить, что фактическая отправка зависит от буферизации PHP, веб-сервера и FastCGI. В документации PHP отмечено, что обработка разрыва соединения и отправка данных зависят от реальной записи в соединение.
Упрощенный пример:
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-cache');
echo "Start\n";
flush();
for ($i = 1; $i <= 5; $i++) {
sleep(1);
echo "Step $i\n";
flush();
}
echo "Done\n";
flush();
Игнорировать разрыв клиента и завершать работу в том же запросе
Если важно, чтобы задача дошла до конца даже если пользователь закрыл вкладку, можно использовать ignore_user_abort(true). PHP по умолчанию обычно завершает скрипт при разрыве клиента, но это поведение можно изменить. Также можно отслеживать состояние соединения через connection_aborted() и connection_status().
Пример:
ignore_user_abort(true); set_time_limit(0); echo "Accepted"; flush(); // Дальше выполняем задачу даже если клиент ушел sleep(15); file_put_contents(__DIR__ . '/log.txt', "finished\n", FILE_APPEND);
Fork процесса через pcntl_fork()
Это уже ближе к реальной параллельности: родительский PHP-процесс может отделить дочерний процесс. Но это работает в Unix/Linux CLI-сценариях и обычно не является хорошей идеей в обычном веб-запросе под FPM/Apache. Функция pcntl_fork() официально существует в расширении Process Control.
Идея:
$pid = pcntl_fork();
if ($pid === -1) {
die('fork failed');
}
if ($pid) {
echo "Parent: response to user";
} else {
sleep(10);
file_put_contents(__DIR__ . '/log.txt', "child finished\n", FILE_APPEND);
exit;
}
Файловая/БД очередь + периодический worker на PHP CLI
Если нужен “чистый PHP”, но уже по-взрослому, делают так:
Это самый надежный вариант без сторонних брокеров.
В отличие от HTTP:
$host = '0.0.0.0'; $port = 8080; $server = stream_socket_server("tcp://{$host}:{$port}", $errno, $errstr); stream_set_blocking($server, false); $clients = []; $handshaked = []; while (true) { $read = $clients; $read[] = $server; // принять новых клиентов // прочитать входящие сообщения // отправить сообщения нужным клиентам }
Если нужна простая и рабочая схема:
Сравнение с другими подходами при использовании чистого пхп
| Подход | Тип асинхронности | Когда использовать |
|---|---|---|
| Queue (Laravel) | фоновая | тяжелые задачи |
terminate() |
после ответа | лог, метрики |
afterResponse() |
микро-задачи | короткие действия |
| Streaming | постепенный ответ | прогресс |
| WebSocket | real-time push | чат, live-данные |
В обычном PHP под веб-сервером “асинхронность” чаще всего не такая, как в Node.js event loop. Обычно это:
То есть для “чистого PHP” самый здравый путь обычно такой:
HTTP → сохранить задачу → сразу ответить → worker делает работу → клиент спрашивает статус.
Основные способы .
1. Очереди (Queue / Job) — главный и самый правильный способ
Laravel рекомендует выносить тяжелые задачи в очереди: отправку email, обработку CSV, генерацию отчетов, импорт, интеграции и другие длительные операции. Идея простая: HTTP-запрос быстро возвращает ответ, а job обрабатывается в фоне worker-процессом. Laravel поддерживает несколько драйверов очередей, включая Redis, SQS и database.
Пример:
// Controller
public function store(Request $request)
{
ProcessOrderJob::dispatch($request->all());
return response()->json([
'status' => 'accepted',
'message' => 'Order queued',
], 202);
}
use Illuminate\Contracts\Queue\ShouldQueue;
class ProcessOrderJob implements ShouldQueue
{
public function __construct(public array $payload) {}
public function handle(): void
{
// тяжелая логика
}
}
Это лучший вариант, когда задача может выполняться отдельно от запроса.
2. dispatchAfterResponse() — выполнить job после отправки ответа браузеру
Laravel умеет отложить dispatch job до момента, когда ответ уже отправлен пользователю. В документации это описано как подход для коротких задач, обычно около секунды, например отправки письма. Это не полноценная очередь в архитектурном смысле, а “сделать сразу после ответа текущим процессом”.
Пример:
public function register()
{
SendWelcomeEmail::dispatchAfterResponse();
return response()->json([
'status' => 'ok',
'message' => 'User created',
]);
}
Или closure:
dispatch(function () {
\Log::info('Executed after response');
})->afterResponse();
return response()->json(['status' => 'ok']);
Подходит для очень коротких пост-действий. Для тяжелой обработки лучше обычная очередь.
3. dispatchSync() / sync driver — не асинхронно, а наоборот синхронно
Это важно не перепутать: Laravel имеет dispatchSync(), но он выполняет задачу в текущем процессе, а не в фоне. Для queueable jobs это идет через sync queue. То есть внешне это job, но реальной асинхронности тут нет.
Пример:
ProcessOrderJob::dispatchSync($data);
Это полезно для единообразия API, но не для ускорения ответа.
4. Batch и chain — сложные фоновые сценарии
Laravel позволяет запускать не только одиночные jobs, но и цепочки (chain) и пакеты (batch). Это удобно, когда асинхронная обработка состоит из нескольких этапов: импорт → обработка → уведомление. У batch также есть dispatch после ответа.
Пример идеи:
Bus::chain([ new PrepareImportJob($fileId), new ParseImportJob($fileId), new NotifyUserJob($userId), ])->dispatch();
Это уже “асинхронный pipeline”.
5. Streamed response — отдавать ответ частями
Laravel умеет формировать streamed response через response()->stream(...). Это другой тип “асинхронности”: пользователь не ждет, пока сформируется весь ответ, а получает его частями. Подходит для прогресса, больших текстов, логов, долгого экспорта, генерации больших файлов.
Пример:
public function stream()
{
return response()->stream(function () {
echo "Start\n";
ob_flush();
flush();
for ($i = 1; $i <= 5; $i++) {
sleep(1);
echo "Step {$i}\n";
ob_flush();
flush();
}
echo "Done\n";
ob_flush();
flush();
}, 200, [
'Content-Type' => 'text/plain',
'Cache-Control' => 'no-cache',
]);
}
Это не очередь, а именно потоковая выдача ответа.
6. SSE / eventStream() — сервер отправляет события по мере появления
Laravel поддерживает Server-Sent Events через response()->eventStream(...). Это уже удобный встроенный способ делать “живые” ответы: прогресс, токены LLM, уведомления, состояние задачи. SSE работает как HTTP-поток с text/event-stream.
Пример:
public function events()
{
return response()->eventStream(function () {
yield "Step 1";
sleep(1);
yield "Step 2";
sleep(1);
yield "Finished";
});
}
Это хороший вариант, если надо именно “ответ приходит постепенно сам”.
7. streamJson() — потоковая выдача JSON
Laravel также поддерживает streamed JSON response. Это полезно, если нужно постепенно отдавать большой JSON без полного формирования в памяти.
8. streamDownload() — асинхронно для пользователя в смысле поэтапной загрузки
Если задача связана с экспортом, Laravel умеет делать потоковую загрузку файла. Это не фоновая очередь, но позволяет пользователю начать получать файл сразу, а не ждать полной генерации.
terminate() подходит, когда нужно:
Типичные кейсы:
Laravel прямо приводит пример, что middleware может делать работу после отправки ответа, а встроенный session middleware сохраняет сессию именно после ответа.
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class LogAfterResponseMiddleware
{
public function handle(Request $request, Closure $next): Response
{
// обычная логика до/во время запроса
return $next($request);
}
public function terminate(Request $request, Response $response): void
{
\Log::info('Response already sent', [
'path' => $request->path(),
'status' => $response->getStatusCode(),
]);
}
}
Потом middleware подключается как обычный middleware — глобально или на маршрут. Исторически документация Laravel указывает, что terminable middleware нужно добавить в HTTP kernel, а сам факт “terminable” определяется просто наличием метода terminate.
Чем отличается terminate() middleware от dispatchAfterResponse()
Они похожи по смыслу: и там, и там работа идет после отправки ответа. Но роль разная:
Практически:
Что происходит под капотом terminate() middleware
Упрощенно цепочка такая:
Это видно и в Laravel API (Illuminate\Foundation\Http\Kernel::terminate и terminateMiddleware), и в Symfony docs: сначала $response->send(), потом $kernel->terminate(...), что и запускает поздние post-response действия.
То есть terminate():
Важная тонкость: это не тот же объект middleware
Laravel отдельно пишет, что при вызове terminate() middleware обычно резолвится заново из контейнера, то есть это может быть новый экземпляр, а не тот, что использовался в handle(). Если нужен один и тот же экземпляр для handle() и terminate(), middleware надо зарегистрировать как singleton.
Это очень важный момент. Например, так не стоит рассчитывать:
class BadMiddleware
{
private $startedAt;
public function handle($request, Closure $next)
{
$this->startedAt = microtime(true);
return $next($request);
}
public function terminate($request, $response)
{
// $this->startedAt может не сохраниться,
// потому что terminate может вызваться на новом экземпляре
}
}
Надежнее:
Потому что terminate() — это все еще часть жизненного цикла того же PHP-процесса запроса, просто после отправки ответа. Для действительно долгих задач Laravel queues подходят лучше, потому что они специально сделаны для deferred processing.
1. Через terminate() — лог и метрика
class ApiTimingMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$request->attributes->set('started_at', microtime(true));
return $next($request);
}
public function terminate(Request $request, Response $response): void
{
$startedAt = $request->attributes->get('started_at');
$durationMs = $startedAt
? round((microtime(true) - $startedAt) * 1000, 2)
: null;
\Log::info('api_request_finished', [
'path' => $request->path(),
'status' => $response->getStatusCode(),
'duration_ms' => $durationMs,
]);
}
}
2. Через queue — тяжелая работа
public function store(Request $request)
{
ProcessImportJob::dispatch($request->all());
return response()->json([
'status' => 'accepted'
], 202);
}
3. Через dispatchAfterResponse() — маленькое пост-действие
public function submit()
{
dispatch(function () {
\Log::info('Light post-response action');
})->afterResponse();
return response()->json(['ok' => true]);
}
Асинхронность на backend — это архитектурный инструмент, позволяющий отделить пользовательский опыт от внутренней обработки данных. В PHP и Laravel она реализуется не через один механизм, а через комбинацию подходов: очереди, пост-обработку после ответа, стриминг и события.
Правильное использование асинхронности позволяет:
Но главное — понимать, что асинхронность — это не цель, а средство.
Ее стоит применять там, где она действительно решает проблему, а не просто “потому что можно”.
Комментарии