Real-time на сайте с Laravel и Centrifugo: зачем нужен WebSocket
- четверг, 14 мая 2026 г. в 00:00:12
Большинство web-приложений исторически строится вокруг простой и надёжной модели: пользователь отправляет запрос, сервер его обрабатывает и возвращает ответ. Эта схема лежит в основе классического HTTP-взаимодействия и отлично подходит для множества привычных задач. Открыть страницу, сохранить форму, получить список заказов, отфильтровать таблицу, отправить комментарий — всё это спокойно укладывается в модель «запрос — ответ».
Проблемы начинаются там, где данные на экране должны меняться без прямого действия пользователя. Страница уже открыта, но состояние системы поменялось где-то на сервере: пришёл новый заказ, изменилась оплата, появился комментарий, оператор ответил в чате, администратор обновил статус заявки, другой пользователь выполнил действие в общей рабочей области. В классической HTTP-модели сервер сам не может «дотянуться» до браузера и сказать: «у меня для тебя новое состояние». Браузеру приходится снова идти на сервер и спрашивать.
Именно здесь появляется задача real-time на сайте.
Real-time в web-приложении — это не декоративный эффект и не попытка сделать интерфейс «модным». Это технический способ доставлять пользователю актуальные изменения сразу после того, как они произошли в системе. Хороший real-time не заменяет обычный HTTP, не отменяет API и не превращает Laravel-приложение в хаотичный поток событий. Он просто добавляет недостающий слой доставки изменений от backend к frontend.
В обычном Laravel-приложении жизненный цикл запроса выглядит предсказуемо. Пользователь нажимает кнопку, браузер отправляет HTTP-запрос, Laravel проходит middleware, контроллер, сервисы, доменную логику, обращается к базе данных и возвращает ответ. После этого соединение закрывается. Всё. Сервер больше ничего не сообщает клиенту, пока клиент сам не сделает следующий запрос.
Для многих задач это абсолютно нормально. Например, пользователь открыл страницу настроек профиля. Данные загрузились. Он изменил имя и нажал «Сохранить». Сервер обработал запрос и вернул результат. Никакого WebSocket в Laravel здесь не требуется.
Но представим другую ситуацию. Пользователь находится на странице заказа и ждёт оплату. Платёжная система отправляет webhook в Laravel. Laravel меняет статус заказа с pending на paid. В базе данных состояние уже актуальное, но открытая страница пользователя об этом не знает. С точки зрения браузера ничего не произошло. Он получил HTML или JSON несколько секунд назад и продолжает показывать старое состояние.
Та же проблема возникает в административных панелях. Менеджер смотрит список заявок. Пока страница открыта, появляются новые заявки. Если интерфейс не обновляется автоматически, менеджер узнает о них только после ручной перезагрузки страницы или после следующего запроса.
Классический HTTP здесь не плохой и не устаревший. Он просто не предназначен для постоянной доставки серверных событий клиенту. Его сильная сторона — простота и предсказуемость. Слабая — отсутствие инициативы со стороны сервера после завершения ответа.
Первое решение, к которому часто приходит команда, — polling. Фронтенд раз в несколько секунд отправляет AJAX-запрос: «Есть ли что-то новое?». Backend отвечает: «Да, вот изменения» или «Нет, ничего нет». На первый взгляд всё выглядит достаточно разумно. Реализовать просто, инфраструктура не меняется, WebSocket-сервер не нужен, отдельный real-time слой тоже.
Например, можно сделать endpoint:
Route::get('/orders/{order}/status', [OrderStatusController::class, 'show']);
И на фронтенде вызывать его каждые пять секунд:
setInterval(async () => { const response = await fetch('/orders/123/status'); const data = await response.json(); updateOrderStatus(data.status); }, 5000);
Для одного пользователя это почти незаметно. Для десяти — тоже. Но дальше начинается арифметика.
Если 1 000 пользователей держат открытую страницу и каждый делает запрос раз в 5 секунд, backend получает примерно 12 000 запросов в минуту. И значительная часть этих запросов будет отвечать: «ничего не изменилось». Приложение тратит ресурсы не на доставку изменений, а на постоянную проверку их отсутствия.

Polling заставляет backend регулярно проверять отсутствие изменений, из-за чего растёт нагрузка на HTTP-сервер, Laravel runtime, middleware, базу данных, Redis, логи и метрики.Polling создаёт нагрузку сразу на несколько уровней:
HTTP-сервер принимает лишние запросы.
PHP-FPM или runtime Laravel обрабатывает повторяющиеся обращения.
Middleware, авторизация и bootstrap приложения запускаются снова и снова.
База данных или Redis получают регулярные проверки.
Логи и метрики заполняются техническим шумом.
Главная неприятность в том, что polling масштабируется не от количества событий, а от количества открытых клиентов. Даже если в системе ничего не происходит, клиенты продолжают спрашивать сервер. В этом и есть слабое место подхода.
Есть ещё один минус: polling не даёт настоящей мгновенности. Если интервал составляет пять секунд, пользователь может увидеть изменение почти сразу, а может только через пять секунд. Если уменьшить интервал до одной секунды, интерфейс станет отзывчивее, но нагрузка возрастёт в пять раз. Выбор неприятный: либо задержка, либо лишняя нагрузка. На практике часто получают оба варианта, просто в умеренной дозировке.
Polling можно использовать для простых и редких обновлений. Но как основа real-time архитектуры он быстро превращается в технический долг.
Real-time нужен не везде. Это важно зафиксировать сразу. Не каждую форму, таблицу и кнопку нужно подключать к WebSocket. Если данные меняются только по действию самого пользователя, обычного HTTP достаточно. Real-time оправдан там, где состояние меняется независимо от текущего пользователя или где задержка заметно ухудшает работу интерфейса.

Real-time уведомления позволяют пользователю сразу видеть новые сообщения, изменения статусов, ошибки платежей и завершение фоновых задач без ручного обновления страницы и постоянных запросов к серверу.Первый очевидный пример — real-time уведомления. Пользователь находится в личном кабинете, а система должна сразу сообщить ему о новом сообщении, смене статуса, ошибке платежа, завершении обработки файла или новом действии другого участника. Без real-time придётся либо заставлять пользователя обновлять страницу, либо использовать polling.

Webhook, очередь или фоновый job могут изменить состояние заказа, платежа или документа без действия пользователя. Real-time уведомление сразу показывает актуальный статус в личном кабинете без ожидания и ручного обновления страницы.Второй пример — статусы заказов и платежей. Это особенно близко Laravel-разработке, потому что многие бизнес-приложения работают с асинхронными процессами. Заказ может перейти в другой статус после webhook от внешней системы. Выплата может пройти обработку через очередь. Документ может быть сгенерирован фоновым job-ом. Пользователь не должен гадать, завершился процесс или нет.

Третий пример — чаты и комментарии. Здесь real-time ожидается самой природой интерфейса. Если один пользователь отправил сообщение, второй должен увидеть его сразу. Формально можно сделать чат на polling, но это будет примерно как возить воду в ведре при наличии водопровода. Работает, да. Но вопросы к инженерной зрелости остаются.

Донат-виджеты, онлайн-счётчики, прогресс сбора, live-ленты и всплывающие алерты должны обновляться мгновенно, потому что зритель видит их как часть публичного пользовательского опыта.Четвёртый пример — виджеты. Например, донат-виджет на стриме, онлайн-счётчик, прогресс сбора, live-лента событий, всплывающие алерты. Такие интерфейсы должны реагировать мгновенно, потому что они часто становятся частью публичного пользовательского опыта.

Пятый пример — административные панели. Операторы поддержки, модераторы, менеджеры заказов, финансовые специалисты и внутренние команды часто работают с данными, которые меняются без их прямого участия. Новая заявка, новый тикет, новый платёж, новая ошибка, новый риск-флаг — всё это должно появляться без ручного обновления страницы.а
Но real-time не должен становиться самоцелью. Если экран открывается раз в день и показывает справочник городов, WebSocket там не нужен. Если настройки профиля меняются только самим пользователем, real-time тоже не нужен. Хорошая архитектура начинается не с вопроса «куда бы прикрутить WebSocket», а с вопроса «какие изменения пользователь должен увидеть без собственного запроса».
Laravel сам по себе не является специализированным WebSocket-сервером. Да, в экосистеме есть разные способы организовать Laravel broadcasting, очереди и события. Но постоянные соединения, подписки, каналы, переподключения, fan-out и доставка сообщений большому количеству клиентов — это отдельная техническая задача. Её лучше выносить в специализированный слой.
Centrifugo как раз закрывает эту роль. Это отдельный real-time сервер, который держит клиентские подключения, управляет каналами и доставляет сообщения подписчикам. Laravel при этом остаётся центром бизнес-логики. И это, что немаловажно, сохраняет нормальное разделение ответственности.
Правильное разделение выглядит так.
Laravel решает, что произошло в системе. Например, заказ оплачен, сообщение создано, уведомление добавлено, статус заявки изменён.
Laravel решает, кто имеет право это увидеть. Например, только владелец заказа, участники чата, администраторы конкретного раздела или пользователи с нужной ролью.
Laravel публикует событие в Centrifugo. Не всю модель, не дамп базы данных, не случайный payload из контроллера, а аккуратное событие с понятным типом и минимальными данными.
Centrifugo доставляет это событие всем клиентам, которые подписаны на соответствующий канал.
Фронтенд получает событие и обновляет интерфейс.
Например, в системе заказов поток может выглядеть так:
Webhook платёжной системы ↓ Laravel проверяет подпись webhook ↓ Laravel меняет статус заказа в базе данных ↓ Laravel публикует событие order.status.changed ↓ Centrifugo доставляет событие в канал пользователя ↓ Фронтенд обновляет статус на странице
Такой подход сохраняет сильные стороны Laravel. Все правила доступа, транзакции, модели, сервисы, очереди и доменные события остаются в привычном backend-контуре. Centrifugo не знает бизнес-смысла заказа или платежа. Он просто эффективно доставляет сообщение.
Это важный архитектурный принцип: Centrifugo не должен становиться вторым backend-ом. В нём не нужно хранить бизнес-логику, проверять сложные права или принимать решения о состоянии системы. Он должен быть транспортным real-time слоем.
Допустим, в Laravel есть обработчик оплаты. После успешной оплаты заказ переводится в статус paid.
Упрощённый пример:
class PaymentWebhookController { public function __invoke(Request $request): JsonResponse { $order = Order::query() ->where('payment_id', $request->input('payment_id')) ->firstOrFail(); $order->update([ 'status' => 'paid', ]); event(new OrderStatusChanged( orderId: $order->id, userId: $order->user_id, status: 'paid', )); return response()->json([ 'success' => true, ]); } }
Событие OrderStatusChanged дальше может быть обработано listener-ом, который отправит публикацию в Centrifugo. Важно, что событие возникает после изменения состояния в системе. Real-time здесь не является источником истины. Источник истины — база данных и доменная логика Laravel.
Само сообщение может быть минимальным:
{ "type": "order.status.changed", "orderId": 123, "status": "paid" }
Фронтенду не нужно получать весь заказ целиком. Он может обновить только статус. А если после переподключения клиента состояние стало неясным, интерфейс может сделать обычный HTTP-запрос и получить актуальный заказ из Laravel.
Это и есть здоровая модель: WebSocket сообщает об изменении, HTTP позволяет восстановить состояние.
Real-time нужен там, где пользователь должен видеть изменения без ручного обновления страницы и без постоянных бессмысленных запросов к серверу. Классическая модель запрос–ответ остаётся надёжной основой web-приложения, но она плохо подходит для серверных событий, которые происходят уже после загрузки страницы.
Polling часто кажется быстрым решением, но его простота обманчива. Он создаёт постоянную нагрузку, плохо масштабируется и всё равно не даёт полноценной мгновенности. Для небольших задач он допустим, но для системной real-time архитектуры это слабый фундамент.
Centrifugo закрывает именно транспортную часть real-time: соединения, каналы, подписки и доставку сообщений. Laravel при этом остаётся главным приложением, где живут бизнес-правила, права доступа, транзакции и события. Такое разделение позволяет добавить real-time в Laravel без разрушения backend-архитектуры