How I used Livewire to replace a websocket events in a real-time application

Today, I want to share a real-world story of how I replaced WebSockets in a Laravel project using Livewire and how you can do the same.
This journey began with a competitive programming platform I built for a nationwide contest involving several universities and developers from all over Brazil. The application is still online and active, and the idea was to allow participants to compete and see the updates in real-time, and some pages like the leaderboard and the live submissions has some simple animations with any updates from the server, that's why the original code use WebSockets for that purpose.
To deploy the project, we were given access to a containerized environment hosted at the Federal University of Santa Maria (UFSM). While the infrastructure was solid, it came with some constraints due to the use of SELinux (Security-Enhanced Linux). Thankfully, adapting the existing Laravel application to run inside that enviroment wasn't too difficult. However, one specific challenge stood out: the WebSocket server couldn’t establish proper communication with the rest of the application inside the container. I don't remember exactly why it wasn't working, but I know I was on a tight schedule and needed a quick solution.
Faced with the WebSocket communication issue, I had to look for an alternative approach that could deliver a similar user experience without relying on WebSockets. That's when I decided to try Laravel Livewire.
Important Considerations
Before diving into the technical steps, it’s important to clarify that Livewire wasn’t a definitive replacement for WebSockets in our system. It was a workaround tailored specifically to the limitations faced during deployment in that environment.
The feature used to keep components in sync with backend events was the Livewire’s polling capability that has a processing overhead that would generally be much lower if handled via WebSockets. So, while Livewire worked well for us in this scenario, it’s not necessarily the best solution for every real-time application.
In short, we recommend this approach for developers who:
- Want to implement event-driven communication in their Laravel applications without the complexity of setting up a WebSocket server.
- Need a quick and functional workaround to keep the application running, perhaps while preparing for a more scalable WebSocket-based solution in the future.
Why Livewire Polling?
Livewire offers a built-in feature called polling, which allows components to periodically communicate with the backend to fetch updated data. I used this to mimic the real-time functionality lost when WebSockets were no longer viable in our environment.
Now, in essence, Livewire’s polling is not much different from traditional JavaScript polling using setInterval
with fetch
or axios
. However, Livewire provides three major advantages out of the box:
- Backend-Integrated Event System: Livewire natively supports events between the frontend and backend, making it easier to respond to server-side changes without writing custom listeners.
- Automatic DOM Diffing: Livewire handles the DOM updates, replacing only the component part, this helps maintain interactivity without full-page refreshes.
- Tight Laravel Integration: Since Livewire is tightly coupled with Laravel, managing component state, authorization, and data flow is straightforward and secure.
First Livewire Implementation: Quick Success, Then Bottlenecks
Our first attempt at using Livewire was fairly straightforward. I turned our entire scoreboard into a Livewire component, applied a wire:poll
directive with a 2-second interval, and just like that we had a live-updating table again.
Here’s a simplified version of the component I built:
Livewire Component – Backend (PHP)
namespace App\Http\Livewire;
use Livewire\Component;
class Scoreboard extends Component
{
public $teams = [];
public function mount()
{
$this->loadTeams();
}
public function loadTeams()
{
// Example static data - in real usage, fetch from DB
$this->teams = [
['name' => 'Team Alpha', 'score' => 150],
['name' => 'Team Beta', 'score' => 120],
['name' => 'Team Gamma', 'score' => 95],
];
}
public function render()
{
return view('livewire.scoreboard');
}
}
Livewire Component – Frontend (Blade)
<div wire:poll.2000ms>
<table class="min-w-full border text-sm text-left">
<thead>
<tr>
<th class="border px-4 py-2">Team</th>
<th class="border px-4 py-2">Score</th>
</tr>
</thead>
<tbody>
@foreach ($teams as $team)
<tr class="border">
<td class="px-4 py-2">{{ $team['name'] }}</td>
<td class="px-4 py-2">{{ $team['score'] }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
With just this setup, we had a functioning real-time scoreboard. But then the problems started to emerge.
The Animation Problem
Previously, it was used JavaScript to handle some subtle but important animations:
- Highlighting a row (e.g., blinking) when a team made a new submission.
- Temporarily swapping CSS classes between the states.
- Triggering celebratory confetti using the Confetti JS library when a successful result came in.
When switched to Livewire, we lost all of that.
Because Livewire re-renders HTML server-side and then replaces the DOM element entirely, all custom JavaScript-based animations were lost in the process. We tried to fix this by controlling the animation timing on the backend and reducing the polling interval significantly to "simulate" real-time feedback. That’s when we ran into the major limitations of this approach.
1. Backend-Controlled Animations Were Inconsistent
Trying to control UI animations from the backend was both inefficient and unreliable. The timing became unpredictable due to network latency and polling delays. A visual effect that was meant to last 500 milliseconds could appear delayed or not at all.
Worse, to make the timing even close to real-time, I had to lower the polling interval to less than a second, introducing unnecessary load on both server and client.
2. Payload Size Was Massive
Each poll sent a full re-rendered version of the component from the backend even if only a single line changed. Our live submissions had over 500 rows, and each request was roughly 60KB in size. Yes, 60KB per request, every 500 milliseconds, regardless of whether any data had changed.
This meant that:
- Bandwidth usage skyrocketed, even when no changes occurred.
- Server processing load increased, as each poll triggered a full component rendering.
- Browser receiving a flood of unnecessary HTML updates, tanking performance.
Polling in Livewire isn’t "diff-aware" like a WebSocket system that sends only what changed. In polling mode, the entire component is always sent and re-rendered. For us, this became a dealbreaker for using Livewire in a real-time, high-frequency production environment.
Emitting Backend Events to JavaScript via Livewire
While revisiting our original WebSocket implementation, I noticed something important: every time a WebSocket message was received, we would pass the data to a JavaScript handler that applied all animations and updates directly to the DOM. This worked beautifully, animations were snappy, timing was perfect, and the server load was minimal.
That’s when the lightbulb went off: we didn’t need Livewire to render the UI. We just needed it to notify us when something happened.
After a bit of digging, I discovered what turned out to be our holy grail: Livewire has its own event system, and you can emit events from the backend to the frontend, and vice versa. That meant I could keep our existing JavaScript logic for WebSocket intact and just trigger the events from PHP whenever something needed to be animated.
So here’s what I did:
- Created a headless Livewire component: it renders nothing in the DOM.
- Added a
wire:poll
directive to it, just to trigger periodic backend requests. - In the backend, we compared timestamps to detect if there were new updates.
- If there was something new, we only need to use
self::dispatch()
to emit a custom JavaScript event with the necessary data. - On the frontend, we listened for that event and passed the data to our old WebSocket animation handlers.
Livewire Component (Backend PHP)
// app/Http/Livewire/EventPoller.php
namespace App\Http\Livewire;
use Livewire\Component;
use Illuminate\Support\Facades\Cache;
use App\Models\Run;
class EventPoller extends Component
{
public $lastChecked = 0;
public function poll()
{
// Simulate stored events (this could be a database query or cache)
$events = Run::where('last_event_at', '>', $lastChecked)->get();
$this->lastChecked = now()->timestamp;
foreach ($events as $event) {
$this->dispatch('live-event', [
'type' => $event['type'],
'data' => $event['data'],
]);
}
}
public function render()
{
return <<<'blade'
<div wire:poll.1000ms="poll"></div>
blade;
}
}
Frontend JavaScript (Listener + Handler)
<script>
function updateSubmission(data) {
// ...
}
$wire.on('live-event', (data) => {
data.forEach(window.updateSubmission)
});
</script>
Why This Works So Well
- No UI bloat: Nothing is re-rendered by Livewire.
- Minimal network usage: We managed to get only 1.3KB for request, which is still big, but good enough for our use case.
- Frontend is in control: All animations are handled directly by JavaScript, the same used for websockets.
This hybrid approach gave us the real-time feedback needed, without the complexity of WebSockets or the performance issues of full-component polling.
Conclusion
At the end of the day, this whole experience showed me that Livewire can be good enough to make a fast and simple solution. The solution I landed on turned out to be simple, clean, and surprisingly effective.
Of course, it's not perfect. WebSockets are still the better option when you need true real-time updates, low latency, and minimal bandwidth usage. But not every project and every scenario needs that. Sometimes, you just need something that works, is easy to build and maintain, and doesn’t require spinning up extra time you doesn't have.
If you’re in a situation like that, this hybrid Livewire approach might be exactly what you need. It's not magic, but it gets the job done.