Polling is useful when your interface needs fresh data, but does not need a permanent connection.
Inertia 3.x makes this pattern simple with usePoll. The composable periodically reloads the current page and merges updated props into your Vue component. It also handles cleanup when the page is unmounted.
That gives you a practical middle ground between a static page and a full WebSocket system.
This guide covers the usePoll API, visibility behavior, manual controls, partial reloads, deferred props, and real-world Laravel examples. The same design principles also help when you build REST API with PHP and need a reliable frontend refresh strategy.
What usePoll does
usePoll calls Inertia’s reload mechanism at a fixed interval. It does not create a new API architecture or require a separate endpoint.
<script setup>
import { usePoll } from '@inertiajs/vue3'
usePoll(5000)
</script>
This reloads the current page every five seconds.
By default, Inertia requests the page props again. That can be more work than necessary for a dashboard with expensive queries. Use the second argument to limit each request to the prop that actually changes.
<script setup>
import { usePoll } from '@inertiajs/vue3'
usePoll(5000, {
only: ['stats'],
})
</script>
The only option uses Inertia partial reloads. Laravel evaluates and returns only the requested prop, while Inertia preserves the rest of the page in memory.
Read the Inertia partial reload documentation for the complete set of reload options.

A Laravel controller for polling
The server-side part stays familiar. Return an Inertia response and wrap changing data in a closure.
<?php
namespace App\Http\Controllers;
use App\Models\Order;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class DashboardController extends Controller
{
public function __invoke(Request $request): Response
{
return Inertia::render('Dashboard', [
'stats' => fn () => [
'orders_today' => Order::whereDate('created_at', today())->count(),
'revenue_today' => Order::whereDate('created_at', today())
->sum('total'),
'updated_at' => now()->toISOString(),
],
'recentOrders' => fn () => Order::latest()->limit(10)->get(),
]);
}
}
The closure is important. Inertia can evaluate lazy props only when the request needs them. During a partial reload for stats, Laravel does not need to run the recentOrders query.
Your Vue page can then poll only the dashboard statistics.
<script setup>
import { usePoll } from '@inertiajs/vue3'
defineProps({
stats: Object,
recentOrders: Array,
})
usePoll(5000, {
only: ['stats'],
preserveScroll: true,
})
</script>
This is one of the cleanest patterns in a Laravel and Vue application. Your controller remains readable, and your polling request stays narrow.
Polling modes: active tabs, hidden tabs, and visible sections
usePoll has visibility-aware behavior, but it helps to separate three ideas.
Active tab polling
When the browser tab is visible, usePoll runs at the interval you provide.
usePoll(3000, {
only: ['stats'],
})
This is useful for live dashboards, operational screens, and notification counts.
Hidden tab polling
When the tab moves into the background, Inertia throttles polling by default. The current documentation describes this as a 90% reduction in polling frequency.
That default protects your server from spending resources on updates that nobody can see.
You can keep polling at full speed with keepAlive.
usePoll(5000, {
only: ['job'],
}, {
keepAlive: true,
})
Use this carefully. A background tab rarely needs second-by-second updates. keepAlive: true makes sense when the browser must continue tracking an operation, such as a deployment monitor or a long-running export.
There is no separate whenHidden option on usePoll. Hidden-tab behavior is controlled through keepAlive.
Viewport visibility with WhenVisible
WhenVisible is different. It watches whether an element enters the viewport. It does not control browser-tab visibility.
Use it to load an expensive section only when the user reaches it.
<script setup>
import { WhenVisible } from '@inertiajs/vue3'
defineProps({
activity: Array,
})
</script>
<template>
<WhenVisible data="activity" :buffer="400">
<div v-for="event in activity" :key="event.id">
{{ event.description }}
</div>
<template #fallback>
<p>Loading activity…</p>
</template>
</WhenVisible>
</template>
The server can defer that prop until the follow-up request.
<?php
namespace App\Http\Controllers;
use App\Models\Activity;
use Inertia\Inertia;
use Inertia\Response;
class ActivityController extends Controller
{
public function index(): Response
{
return Inertia::render('Activity/Index', [
'summary' => Activity::latest()->limit(5)->get(),
'activity' => Inertia::defer(
fn () => Activity::latest()->limit(50)->get()
),
]);
}
}
See the Inertia load-when-visible documentation and deferred props documentation for more options.
Manual start and stop controls
Polling starts automatically when the component mounts. You can disable that behavior with autoStart: false.
<script setup>
import { usePoll } from '@inertiajs/vue3'
const { start, stop, polling } = usePoll(
2000,
{
only: ['stats'],
},
{
autoStart: false,
mode: 'rest',
},
)
</script>
<template>
<button v-if="polling" @click="stop">
Pause updates
</button>
<button v-else @click="start">
Resume updates
</button>
</template>
The returned polling value tells you whether the poll is running. It does not indicate whether a request is currently in flight.
The mode option controls concurrent requests:
-
overlapis the default. A new request starts on every tick. -
cancelaborts the previous request when a new tick starts. -
restwaits for the previous request to finish before starting the next interval.
For database-heavy dashboards, rest is often a sensible choice. It prevents slow requests from stacking up.
Inertia also stops the poll when the component unmounts. You do not need to manage a timer manually.
Deferred props and polling together
Deferred props solve the initial loading problem. Polling solves the freshness problem.
You can use both in the same page:
- Defer a costly prop.
- Load it when the section becomes visible.
- Mount a child component that polls that prop.
- Request only that prop on each tick.
For example, the parent page can defer an activity feed:
return Inertia::render('Dashboard', [
'activity' => Inertia::defer(
fn () => Activity::latest()->limit(50)->get(),
'dashboard-data'
),
]);
The visible section can render the data through Deferred.
<script setup>
import { Deferred } from '@inertiajs/vue3'
import ActivityPanel from './ActivityPanel.vue'
</script>
<template>
<Deferred data="activity">
<template #fallback>
<div>Loading activity…</div>
</template>
<ActivityPanel />
</Deferred>
</template>
The child component can keep the loaded prop fresh.
<script setup>
import { usePoll } from '@inertiajs/vue3'
defineProps({
activity: Array,
})
usePoll(10000, {
only: ['activity'],
preserveScroll: true,
preserveState: true,
}, {
mode: 'rest',
})
</script>
This approach avoids running the feed query during the first page render. It also avoids reloading unrelated props during each poll.

Practical example: notification badges
Notification badges rarely need WebSocket-level immediacy. A 15-second refresh is usually enough.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class NotificationController extends Controller
{
public function index(Request $request): Response
{
return Inertia::render('Notifications/Index', [
'notifications' => fn () => $request->user()
->notifications()
->latest()
->limit(25)
->get(),
'notificationMeta' => fn () => [
'unread' => $request->user()
->unreadNotifications()
->count(),
],
]);
}
}
Poll only the badge metadata.
<script setup>
import { usePoll } from '@inertiajs/vue3'
defineProps({
notificationMeta: Object,
notifications: Array,
})
usePoll(15000, {
only: ['notificationMeta'],
preserveScroll: true,
})
</script>
<template>
<span v-if="notificationMeta.unread">
{{ notificationMeta.unread }}
</span>
</template>
The full notification list remains untouched until the user visits the page or performs another action.
Practical example: queued job progress
Laravel batches expose progress information through Bus::findBatch. That makes them a natural fit for polling.
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Bus;
use Inertia\Inertia;
use Inertia\Response;
class ImportProgressController extends Controller
{
public function show(string $batchId): Response
{
return Inertia::render('Imports/Progress', [
'progress' => function () use ($batchId) {
$batch = Bus::findBatch($batchId);
return [
'percent' => $batch?->progress() ?? 0,
'finished' => $batch?->finished() ?? false,
'failed' => $batch?->failedJobs ?? 0,
];
},
]);
}
}
The Vue page can stop polling after completion.
<script setup>
import { usePoll } from '@inertiajs/vue3'
import { watch } from 'vue'
const props = defineProps({
progress: Object,
})
const { stop } = usePoll(2000, {
only: ['progress'],
}, {
mode: 'rest',
})
watch(
() => props.progress.finished,
(finished) => {
if (finished) {
stop()
}
},
)
</script>
<template>
<progress :value="progress.percent" max="100" />
<p>{{ progress.percent }}% complete</p>
<p v-if="progress.failed">
{{ progress.failed }} jobs failed.
</p>
</template>
For a small number of users, this is straightforward and reliable. Laravel’s queue documentation also provides the foundation for inspecting batches and reporting progress.
usePoll versus Reverb, Echo, and setInterval
Polling is not a replacement for every real-time architecture.
| Approach | Best for | Main trade-off |
|---|---|---|
usePoll |
Dashboards, badges, moderate-frequency status updates | Updates arrive on an interval |
| Reverb and Echo | Chat, collaboration, instant job events, live notifications | Requires WebSocket infrastructure |
Manual setInterval
|
Highly custom browser-only behavior | You manage cleanup, visibility, and concurrency |
Laravel Reverb provides WebSocket communication, while Laravel Echo subscribes to channels and listens for broadcast events. The Laravel broadcasting documentation covers that architecture.
Use Reverb and Echo when the server should push an update immediately. Use usePoll when a small delay is acceptable and you want to keep the implementation inside the normal Inertia request cycle.
Avoid manual setInterval unless you need behavior that usePoll cannot provide. A hand-written timer must handle component teardown, duplicate requests, browser visibility, failed requests, and stale state.
That is a lot of plumbing for a feature Inertia already understands.

A practical polling checklist
Before shipping a poll, check these points:
- Poll only the prop that changes with
only. - Use lazy closures in the Laravel controller.
- Choose an interval based on user value, not habit.
- Accept hidden-tab throttling unless background updates are essential.
- Use
mode: 'rest'for slow or expensive requests. - Stop polling when the task reaches a terminal state.
- Use
WhenVisiblefor expensive viewport-based sections. - Choose Reverb and Echo when instant server push matters.
usePoll is small, but it fits neatly into Inertia’s larger approach. Laravel supplies the data. Inertia reloads only what changed. Vue updates the interface without losing its local state.
That balance keeps live features useful, predictable, and joyful to build.