You've got a dashboard with a few counters — revenue today, new signups, open tickets — and they go stale the moment the page loads. The usual reflex is to hand-roll a setInterval plus fetch, or stand up a whole websocket stack, just to keep three numbers moving. Livewire 4 ships polling in the box: wire:poll re-renders a component on an interval with zero custom JavaScript. Here's how to wire it up, tune it so it doesn't hammer your server, and scope it to just the metrics.
Create the dashboard component#
Start with a plain Livewire 4 component and expose each metric as a computed property. Computed properties are queried on demand and memoised for the length of a single request, which is exactly what you want when a re-render needs fresh numbers. Scaffold it with Artisan:
php artisan make:livewire metrics-dashboard
That gives you a single-file component at resources/views/components/⚡metrics-dashboard.blade.php (the ⚡ prefix is a Livewire 4 convention you can switch off in config/livewire.php). Fill in the metrics:
<?php // resources/views/components/⚡metrics-dashboard.blade.php
use App\Models\Order;
use App\Models\Ticket;
use App\Models\User;
use Livewire\Attributes\Computed;
use Livewire\Component;
new class extends Component {
#[Computed]
public function revenueToday(): string
{
// Amounts stored in pennies, so divide before formatting.
return number_format(
Order::whereDate('paid_at', today())->sum('total') / 100,
2,
);
}
#[Computed]
public function signupsToday(): int
{
return User::whereDate('created_at', today())->count();
}
#[Computed]
public function openTickets(): int
{
return Ticket::where('status', 'open')->count();
}
};
?>
<div>
{{-- Metric cards go here --}}
</div>
Render the metric cards#
Drop the cards into the component's root element and read each computed property with the $this-> prefix — that's what tells Livewire to call the method and cache the result for this request. A three-column Tailwind grid is plenty:
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div class="rounded-xl border border-gray-200 p-5">
<p class="text-sm text-gray-500">Revenue today</p>
<p class="mt-2 text-3xl font-semibold">£{{ $this->revenueToday }}</p>
</div>
<div class="rounded-xl border border-gray-200 p-5">
<p class="text-sm text-gray-500">New signups</p>
<p class="mt-2 text-3xl font-semibold">{{ $this->signupsToday }}</p>
</div>
<div class="rounded-xl border border-gray-200 p-5">
<p class="text-sm text-gray-500">Open tickets</p>
<p class="mt-2 text-3xl font-semibold">{{ $this->openTickets }}</p>
</div>
</div>
Render it anywhere with <livewire:metrics-dashboard />. If one of those queries is genuinely expensive, don't run it on every tick — cache it across requests with Livewire 4's #[Computed(persist: true)] so the poll reads a warm value instead of pounding the database.
Add wire:poll to refresh the metrics#
Now make it live. Add wire:poll to the component's root element and Livewire re-renders it on an interval — no JavaScript, no event wiring:
<div wire:poll>
{{-- metric cards --}}
</div>
By default that fires every 2.5 seconds, re-running render() and re-evaluating your computed properties each time (memoisation is per-request, so every poll gets fresh numbers). The default is usually too aggressive for a dashboard, so set the cadence explicitly, and pass a method name when you want a specific hook on each tick:
{{-- Poll every 10 seconds instead of the 2.5s default --}}
<div wire:poll.10s>
{{-- metric cards --}}
</div>
{{-- Call refreshMetrics() on each tick --}}
<div wire:poll.10s="refreshMetrics">
{{-- metric cards --}}
</div>
With computed properties you rarely need the method form — a bare wire:poll already recomputes on re-render. Reach for wire:poll="refreshMetrics" when a tick has to do something beyond re-rendering: dispatch an event, warm a cache, or write into a public property. This is the same engine Filament sits on top of; its chart widgets poll on an interval and defer their first load exactly like this.
Throttle wire:poll with the visible modifier#
Polling is a request per tick per visitor, so the cheapest win is to not poll when nobody's looking. The .visible modifier only polls while the element is in the viewport, and Livewire already throttles background tabs by around 95% on its own:
<div wire:poll.15s.visible>
{{-- metric cards --}}
</div>
If a dashboard lives below the fold, .visible means it won't start polling until the user scrolls it into view, and it stops again when they scroll away. If you actually need a counter to stay live even in a background tab — a trading screen on a second monitor, say — opt out of the throttling with .keep-alive:
<div wire:poll.15s.keep-alive>
{{-- metric cards --}}
</div>
That viewport-aware behaviour is the same idea behind wire:intersect, which fires actions as elements scroll into view using an IntersectionObserver under the hood.
Isolate wire:poll inside a Livewire island#
Here's the trap that bites people: wire:poll re-renders the whole component, no matter which element you put it on. Stick it on your metric grid and every chart, table and filter in the same component re-renders every 15 seconds too. In Livewire 4 the fix is an island — wrap just the metrics in @island and only that region re-renders:
<div>
<h1 class="text-2xl font-semibold">Operations</h1>
@island
<div wire:poll.15s.visible class="grid grid-cols-1 gap-4 sm:grid-cols-3">
{{-- metric cards --}}
</div>
@endisland
{{-- Expensive charts and tables live out here and are NOT touched by the poll --}}
<livewire:revenue-chart />
</div>
Now the poll re-renders only the island, and the computed properties inside it run only when the island itself refreshes — the chart below is left alone. Two caveats worth knowing before you lean on this: an island can't sit inside a @foreach or @if, and it can't read template variables declared outside it — it sees component properties and computed properties, nothing more. If you lazy-load an island, pair it with a skeleton loader that matches the island's layout so the first paint doesn't jump.
Swap to websockets when polling outgrows itself#
Do the arithmetic before you ship: 200 people watching a dashboard at wire:poll.5s is roughly 40 requests a second hitting Laravel just to move a few counters. Polling is perfect for low-traffic internal tools and cadences of five seconds or slower, but it doesn't scale to sub-second updates or high fan-out. When you cross that line, stop asking and start pushing — Laravel Reverb gives you first-party websockets, and its database driver runs without Redis so the barrier to entry is low. One last gotcha with islands: island requests run in parallel with the root component, so if two are in flight at once, the last response to land wins the state battle — keep the state each island owns separate. If you want to push this pattern further, the full guide to Livewire 4 islands and lazy-loading covers deferring the expensive regions entirely.
FAQ#
How often does wire:poll refresh by default?
Every 2.5 seconds. Adding a bare wire:poll to an element re-renders that component every 2.5s and keeps its output current. For a dashboard that's usually more often than you need, so it's worth setting an explicit interval rather than leaving it on the default.
How do I change the wire:poll interval?
Append the duration to the directive as a modifier. Use wire:poll.10s for ten seconds or wire:poll.15000ms for milliseconds. Longer intervals are the single biggest lever for cutting request volume, so pick the slowest cadence your users will tolerate.
How do I poll only part of a Livewire component?
Wrap that part in an @island and put wire:poll inside it. A normal wire:poll re-renders the entire component regardless of which element carries it, but an island scopes the re-render to just that region, leaving the rest of the page untouched. Extracting the region into a child component achieves the same isolation the older way.
Does wire:poll keep running in a background tab?
It keeps running but heavily throttled. Livewire automatically cuts polling by roughly 95% when the tab is in the background to save server requests. If you genuinely need it to keep firing at full rate in a background tab, add the .keep-alive modifier to opt out of that throttling.
Is wire:poll bad for performance?
It's not bad, it's just chatty — every tick is a full round-trip to the server for each visitor. That's fine for internal tools and modest traffic, and you can tame it with a longer interval, the .visible modifier, and islands so only the metrics re-render. It becomes a problem at high concurrency or very short intervals, which is your cue to switch to websockets.
When should I use websockets instead of polling?
Reach for websockets once you need sub-second updates, have hundreds of people on the same live view, or are pushing high-frequency events. At that scale a request-per-tick model like polling generates too much load, and a push-based transport like Laravel Reverb is both cheaper and lower-latency. For a handful of counters refreshing every few seconds, polling is the simpler, correct choice.