Laravel Daily's

Staying Fresh: Real-Time Polling in Inertia 3.x with usePoll

hero image

A dashboard feels current when its data changes without a manual refresh. Inertia 3.x makes that behavior simple with usePoll.

The helper periodically reloads the current page. It uses the same server-side route and controller that rendered the page. You keep Laravel’s routing model while adding a small amount of Vue logic.

This approach works well for activity feeds, job monitors, support queues, analytics dashboards, and operational screens. It also fits Laravel’s position as a productive PHP web framework with a complete set of PHP developer tools.

What usePoll does

usePoll sends periodic Inertia reload requests. The only required argument is an interval in milliseconds.

<script setup>
import { usePoll } from '@inertiajs/vue3'

usePoll(5000)
</script>

This example reloads the current page every five seconds.

The response can include every page prop, but most dashboards only need a small subset. Use the only option to request specific props during each poll.

<script setup>
import { usePoll } from '@inertiajs/vue3'

usePoll(5000, {
    only: ['stats', 'events'],
    preserveScroll: true,
})
</script>

The second argument accepts the same request options as router.reload. You can also provide a function when the request needs current component state.

Inertia automatically stops polling when the page is unmounted. You do not need to manage an interval yourself.

Diagram showing Inertia polling requests between a browser dashboard and Laravel

Polling modes: interval and concurrency

The interval controls how often polling attempts to start. It does not always determine how often requests reach your server.

Request duration matters. A slow query or busy server can still be processing the previous request when the next tick arrives. Inertia 3.x provides three concurrency modes.

overlap: The default mode

overlap starts a new request on every tick. It does so even when the previous request remains in flight.

usePoll(2000, {
    only: ['events'],
}, {
    mode: 'overlap',
})

This mode can work when requests are fast and the endpoint is inexpensive. It can also create a queue of concurrent requests when the server slows down.

Avoid it for expensive reports or queries with unpredictable response times.

cancel: Keep the newest request

cancel aborts an in-flight request when the next polling tick begins.

usePoll(2000, {
    only: ['stats'],
}, {
    mode: 'cancel',
})

This mode suits dashboards where the newest data matters more than completing every intermediate request. It also limits the browser to one active polling result at a time.

Use it when users care about the latest state rather than every change.

rest: Never overlap requests

rest measures the interval after the previous request finishes. The next request starts only after that interval passes.

usePoll(2000, {
    only: ['events'],
}, {
    mode: 'rest',
})

If a request takes 800 milliseconds, the next request starts 2,000 milliseconds after completion. The effective cycle lasts 2,800 milliseconds.

This is usually the safest default for a live dashboard. It prevents slow requests from stacking up while preserving a predictable pause between updates.

Visibility options: what exists in Inertia 3.x

The terms whenVisible, whenTabVisible, and whenWindowFocused describe different browser behaviors. Inertia handles them through different features.

They are not interchangeable usePoll options.

whenVisible means viewport visibility

Inertia’s WhenVisible component uses the browser’s Intersection Observer API. It loads deferred props when an element enters the viewport.

It does not control polling.

<script setup>
import { WhenVisible } from '@inertiajs/vue3'
</script>

<template>
    <WhenVisible data="olderEvents" :buffer="500">
        <template #fallback>
            <div>Loading older events...</div>
        </template>

        <EventArchive :events="olderEvents" />
    </WhenVisible>
</template>

The buffer value starts loading data before the element becomes visible. The always prop triggers loading each time the element re-enters the viewport.

This makes WhenVisible useful for an event archive or infinite scroll list. It is not a replacement for usePoll.

Read the official Inertia load-when-visible documentation for the complete component API.

whenTabVisible is built-in throttling

Inertia does not expose a whenTabVisible option on usePoll.

Instead, the poll helper throttles requests by 90 percent when the browser tab moves into the background. This reduces unnecessary traffic while the user views another tab.

For most applications, the default behavior is enough.

If the dashboard must continue polling in the background, use keepAlive.

usePoll(5000, {
    only: ['alerts'],
}, {
    keepAlive: true,
})

Use this carefully. Background polling can consume server resources for a page the user is not actively viewing.

whenWindowFocused requires custom control

Inertia does not provide a whenWindowFocused option either. If polling should run only while the browser window has focus, disable automatic startup and connect the helper to browser events.

<script setup>
import { onBeforeUnmount, onMounted } from 'vue'
import { usePoll } from '@inertiajs/vue3'

const { start, stop } = usePoll(
    5000,
    {
        only: ['stats', 'events'],
        preserveScroll: true,
    },
    {
        autoStart: false,
        mode: 'rest',
    },
)

const syncPolling = () => {
    const active = document.visibilityState === 'visible' && document.hasFocus()

    active ? start() : stop()
}

onMounted(() => {
    window.addEventListener('focus', syncPolling)
    window.addEventListener('blur', syncPolling)
    document.addEventListener('visibilitychange', syncPolling)

    syncPolling()
})

onBeforeUnmount(() => {
    window.removeEventListener('focus', syncPolling)
    window.removeEventListener('blur', syncPolling)
    document.removeEventListener('visibilitychange', syncPolling)
})
</script>

This combines tab visibility and window focus. Polling starts when both conditions are true.

The built-in background throttle remains useful when you want polling to continue at a reduced rate. Custom start and stop control is better when polling should pause completely.

Build a live Laravel dashboard

Consider an operations dashboard that shows active jobs, recent failures, and the latest system events.

Laravel provides the page through a normal route and controller. Inertia then reloads selected props on each poll.

The Laravel route

Define the route in routes/web.php.

use App\Http\Controllers\OperationsDashboardController;
use Illuminate\Support\Facades\Route;

Route::get('/operations', [OperationsDashboardController::class, 'index'])
    ->middleware('auth')
    ->name('operations.index');

Laravel supports controller routes, middleware groups, named routes, and rate limiting through its standard routing features.

The controller

The controller returns the initial dashboard state.

<?php

namespace App\Http\Controllers;

use App\Models\Event;
use App\Models\Job;
use Inertia\Inertia;
use Inertia\Response;

class OperationsDashboardController extends Controller
{
    public function index(): Response
    {
        return Inertia::render('Operations/Dashboard', [
            'stats' => fn () => [
                'activeJobs' => Job::where('status', 'running')->count(),

                'failedLastHour' => Job::where('status', 'failed')
                    ->where('created_at', '>=', now()->subHour())
                    ->count(),

                'processedToday' => Job::where('status', 'completed')
                    ->whereDate('created_at', today())
                    ->count(),
            ],

            'events' => fn () => Event::query()
                ->latest()
                ->limit(20)
                ->get(['id', 'type', 'message', 'created_at']),
        ]);
    }
}

The closures defer evaluation until the props are requested. The only list on the Vue side keeps each partial reload focused.

For a larger application, cache expensive aggregates or move hot counters into Redis. Laravel’s Redis integration supports direct commands, caching, pipelines, and publish-subscribe workflows.

The Vue page

The page receives the same props during the initial visit and every partial reload.

<script setup lang="ts">
import { usePoll } from '@inertiajs/vue3'

type Stats = {
    activeJobs: number
    failedLastHour: number
    processedToday: number
}

type EventItem = {
    id: number
    type: string
    message: string
    created_at: string
}

defineProps<{
    stats: Stats
    events: EventItem[]
}>()

usePoll(5000, {
    only: ['stats', 'events'],
    preserveScroll: true,
}, {
    mode: 'rest',
})
</script>

<template>
    <section>
        <header>
            <h1>Operations</h1>
            <p>Updated automatically every five seconds.</p>
        </header>

        <div class="grid">
            <article>
                <span>Active jobs</span>
                <strong>{{ stats.activeJobs }}</strong>
            </article>

            <article>
                <span>Failures in the last hour</span>
                <strong>{{ stats.failedLastHour }}</strong>
            </article>

            <article>
                <span>Processed today</span>
                <strong>{{ stats.processedToday }}</strong>
            </article>
        </div>

        <ul>
            <li v-for="event in events" :key="event.id">
                <strong>{{ event.type }}</strong>
                <span>{{ event.message }}</span>
            </li>
        </ul>
    </section>
</template>

This page does not need a separate polling endpoint. It does not duplicate authorization logic. It does not introduce a second response format.

The existing Laravel page route remains the source of truth.

Laravel and Vue illustration showing a controller, database, Redis, and live feed

Polling versus a REST API

Polling through Inertia works well when the data belongs to the current page. The browser requests a partial reload, and Laravel returns updated page props.

A separate API makes more sense when several clients need the same data. Mobile apps, third-party integrations, and independent frontends may need JSON endpoints with their own authentication and versioning.

If you need to build a REST API with PHP, Laravel can install API routing and Sanctum authentication through php artisan install:api.

Choose the smallest interface that fits the product:

  • Use usePoll for page-bound updates.
  • Use an API for independent clients.
  • Use broadcasting with Laravel Reverb when updates must arrive immediately.
  • Use Laravel Pulse or Nightwatch when the main need is application monitoring.

Production checks

Polling is simple, but every browser tab creates traffic. Start with a reasonable interval.

A five-second interval creates twelve requests per minute for one active tab. Multiply that by a team of users and several open dashboards.

Keep the payload small with only. Prefer rest for endpoints with variable response times. Let background throttling work unless users need uninterrupted updates.

Protect expensive routes with authentication and Laravel rate limiting. Index columns used by frequent queries, such as status, created_at, and tenant identifiers.

Finally, observe the endpoint in production. Laravel Cloud, Forge, and Nightwatch can support the deployment and monitoring side of the application.

The best live dashboard is not the one that requests data most often. It is the one that stays useful, current, and quiet when no one is watching.

Previous
Forms Without Pain: Validation, Uploads, and Errors in Laravel + Vue + Inertia 3.x
Next
Build a Speech-to-Text API with Laravel and the AI SDK: Audio to Actionable Data