Laravel Pulse helps identify patterns worth investigating: slow application work, repeated exceptions and changes in usage. A dashboard is useful when its measurements have a known scope and lead to an action. An empty card can mean no qualifying events, a disabled recorder or broken ingestion; it does not establish that everything is healthy.

This guide targets Laravel 12 and Pulse 1.8.1. The worked example is a hypothetical catalogue synchronization that returns accepted and failed row counts, followed by a separate job that writes a report. Its purpose is to distinguish business outcomes, processing time and queue behavior. It describes a proposed application design, not monitoring installed on this website.

Give Pulse a specific role

Use the tool that answers the current question
ToolUseful questionBoundary
PulseWhich routes, jobs or operations show recurring problems?Aggregated observations help choose an investigation; they do not reconstruct every execution.
HorizonAre Redis queue workers processing work, and which jobs fail or wait?Queue monitoring and worker configuration do not establish that a completed job produced correct business data.
TelescopeWhat happened during a captured request or event?Detailed debugging records need deliberate capture, filtering and access controls.
Distributed tracingWhere did one operation spend time across services?Related spans need instrumentation and propagated context; aggregate duration alone supplies neither.

Laravel documents Telescope's debugging scope, while OpenTelemetry's trace model explains connected spans. Keep those distinctions when someone asks for “APM”: decide whether they need trends, detailed events, traces, alerts or all four. Our Horizon queue guide covers the report-job side of this example.

Install against the intended database and lock access

The Laravel 12 documentation lists MySQL, MariaDB and PostgreSQL as Pulse's first-party storage options. An application using another engine can configure a separate Pulse connection. A lightweight SQLite application test does not establish compatibility or operational behavior for that production storage choice.

In a development checkout with the intended connection configured, the installation sequence is:

composer require laravel/pulse:1.8.1
php artisan vendor:publish --provider="Laravel\Pulse\PulseServiceProvider"
php artisan migrate

Review published migrations and the Composer lockfile before deployment. The migration command runs pending migrations in the application; it is not limited to Pulse. For a separate connection, configure PULSE_DB_CONNECTION and check where the published migration will create its tables before running it.

The dashboard uses /pulse by default. Define viewPulse in AppServiceProvider::boot for the application's real authentication model. This example uses an application-owned configuration array of approved user IDs:

use App\Models\User;
use Illuminate\Support\Facades\Gate;

// Inside AppServiceProvider::boot():
Gate::define('viewPulse', fn (User $user): bool => in_array(
    (string) $user->getAuthIdentifier(),
    config('monitoring.pulse_user_ids', []),
    true,
));

Create config/monitoring.php with a pulse_user_ids array of approved IDs stored as strings; an empty array denies access. The typed user argument excludes guests. Pulse 1.8.1's default gate allows the local environment; this custom gate replaces that default and applies locally too. Adapt the policy to your existing permissions system, keep Pulse's authorization middleware and test guest, ordinary-user and authorized-user access in a non-local environment.

Choose what “slow” means before reading the cards

Pulse 1.8.1's published configuration sets the four slow recorders to a 1,000-millisecond threshold and a sample rate of 1 by default. Those defaults are not service-level objectives. A background export and an interactive checkout have different acceptable durations.

For an initial investigation, these are illustrative configuration values, not recommended targets for every application:

PULSE_SLOW_REQUESTS_THRESHOLD=1000
PULSE_SLOW_JOBS_THRESHOLD=1000
PULSE_SLOW_QUERIES_THRESHOLD=250
PULSE_SLOW_OUTGOING_REQUESTS_THRESHOLD=500
PULSE_USER_REQUESTS_SAMPLE_RATE=1
PULSE_STORAGE_KEEP="7 days"

The SlowJobs recorder measures time between job-processing events and skips the synchronous queue connection. It does not measure how long a job waited before starting. The outgoing-request recorder instruments Laravel's HTTP client; calls made through an unrelated SDK or raw curl need separate consideration.

Choose thresholds, ignored patterns and grouping around a question you intend to answer. Keep the same configuration while comparing before and after a change. A lower threshold can increase recorded volume even if the application has not become slower.

Record a business operation with bounded labels

Suppose the catalogue operation returns ['accepted' => 12, 'failed' => 1]. The operation returned normally, but one row still needs attention. A queue-success counter alone cannot describe that result. Define three outcome labels: completed when no rows failed, partial for a normal return with one or more rejected rows, and failed when the operation throws. Here, partial includes a returned summary in which all rows were rejected.

Save this helper as app/Support/measureCatalogueSync.php in your application. Its callback contract returns non-negative integer counts under accepted and failed; adapt your sync service to that contract. It measures the callback, returns its summary unchanged and allows its original exception to propagate:

<?php

use Laravel\Pulse\Facades\Pulse;

/** @param callable(): array{accepted: int, failed: int} $sync */
function measureCatalogueSync(callable $sync): array
{
    $started = hrtime(true);
    $outcome = 'failed';

    try {
        $summary = $sync();
        $outcome = $summary['failed'] > 0 ? 'partial' : 'completed';

        return $summary;
    } finally {
        $elapsedMs = (int) round((hrtime(true) - $started) / 1_000_000);

        rescue(
            fn () => Pulse::record('catalogue_sync_duration', $outcome, $elapsedMs)
                ->avg()
                ->count(),
            report: false,
        );
    }
}

For a small demonstration inside a normal Artisan command, load the helper and invoke it with an invented summary. This callback imports nothing; it only exercises the measurement wrapper:

require_once app_path('Support/measureCatalogueSync.php');

$summary = measureCatalogueSync(
    fn (): array => ['accepted' => 12, 'failed' => 1],
);

The normal Artisan command lifecycle ingests the recorded entry when the command finishes; an interactive shell or a custom test harness needs its own lifecycle consideration.

The entry aggregation API requires requesting avg and count when recording. The fixed keys avoid creating a separate aggregate for every product, customer or run. The count means recorded attempts; a retried batch can contribute more than once. Duration includes the callback's work and waiting, but excludes time spent queued before the callback starts.

The rescue call isolates a direct telemetry failure from the business result. It does not create an alert or guarantee that the metric reached storage. Hard process termination can bypass the finally block, so this metric is not a durable completion ledger.

Recording a custom type does not add a card automatically. In a custom component extending Laravel\Pulse\Livewire\Card, use the aggregate retrieval method for the card's selected period:

$durations = $this->aggregate(
    'catalogue_sync_duration',
    ['avg', 'count'],
);

Render each outcome's key, mean milliseconds and attempt count in that card's view, then add the component to the published Pulse dashboard. The snippet is the retrieval expression, not a complete Livewire component. Label the unit and period visibly, and present “no recorded attempts” separately from zero milliseconds.

We checked the helper in an isolated Laravel 12.40.2 application with Pulse 1.8.1, PHP 8.3.6 and MariaDB 10.11.14. It preserved callback results and the original exception, stored all three outcome labels and preserved business behavior when a recording failure was injected. The demonstration also persisted through the normal Artisan lifecycle. Separate synthetic 100- and 200-millisecond entries returned a mean of 150 and count of two from SQL. The typed gate and authorization middleware denied guests and unlisted users while allowing the approved user in a non-local environment. These checks did not cover browser login, a complete custom dashboard or production load.

Interpret a mean alongside volume and outcomes

Two illustrative attempts lasting 100 and 200 milliseconds produce a mean of 150 milliseconds and a count of two. That arithmetic says nothing about a percentile. For example, durations of 100, 100 and 4,900 milliseconds average 1,700 milliseconds; the mean hides how differently those attempts behaved.

Compare the three outcome groups together. A falling completed-attempt mean can be misleading if slow operations now fail early. A growing attempt count can mean retries rather than increased useful throughput. Keep the authoritative row outcomes and run identifiers in the business system; use sanitized logs or traces to investigate one run.

Sampling needs the same care. A built-in recorder configured at 0.1 captures approximately one tenth of eligible events; supported cards may show scaled estimates. Rare failures can disappear from a small sample. The direct custom Pulse::record calls above have no sampling policy, and changing a built-in recorder's sample rate does not add one to this helper.

Investigate the catalogue workflow in stages

Imagine staff report that catalogue updates arrive late. First identify the release, time window and whether “late” means the sync itself or the subsequent report becoming available. The following are hypothetical observations and investigation choices, not measured results:

Choose the next check from the observed boundary
ObservationPlausible explanationNext check
Sync duration rises; outgoing calls are slowProvider latency or retries may dominate the callback.Inspect sanitized provider status and retry logs for the same window; distinguish throttling from application work.
Sync finishes promptly; reports appear lateThe later report job may be waiting or failing.Use Horizon to examine the reports queue and the report job's outcome.
Partial outcomes rise without exceptionsThe source may contain rejected rows that the application handled normally.Read the business rejection reasons and compare the input version.
All cards stop changingTraffic may have stopped, or telemetry ingestion may be broken.Check actual workload, recorder settings, ingestion health and the newest stored observation.

Change one suspected cause, repeat a representative workload and compare its useful outcome as well as its timing. Do not increase worker concurrency solely because a card is red: more simultaneous calls can worsen an already throttled upstream service.

Operate the monitoring path and protect its data

With direct storage ingestion, Pulse writes to its configured database. Redis ingestion adds a buffer and requires a supervised php artisan pulse:work process; it still persists Pulse data to SQL. Use a separate Redis connection from the application's job queue. Server-resource cards additionally require php artisan pulse:check on the servers being observed.

Both are long-running processes. Include php artisan pulse:restart, a working shared cache for its signal and process supervision in deployment planning. Our Laravel CI/CD guide explains why a code release must also account for long-running processes.

Pulse 1.8.1 defaults to seven-day storage retention and uses a trimming lottery during ingestion. Redis-ingest retention is configured separately. Treat trimming as operational cleanup, not an exact-time erasure guarantee or a long-term audit archive; verify database size and cleanup behavior under the actual workload.

Review what every enabled recorder stores. SQL bindings are not interpolated by the slow-query recorder, but raw SQL can still contain sensitive literals. Outgoing URLs, cache keys and resolved user details also need review. Group or exclude identifiers and secrets before collection where supported; reducing cardinality alone does not establish privacy.

Pulse normally suppresses its capture failures to avoid disrupting the application. Configure Pulse::handleExceptionsUsing to report a minimal, sanitized failure through an independent logging path, with rate limiting and no recursive Pulse dependency. Test that reporting failure cannot mask the original application exception. Keep a separate health check for the ingestion process and stale data.

For a practical first pass, bring one problematic workflow, its queue arrangement and an example failure to our Laravel development service. Define the question, add the smallest useful measurement and verify that its failure mode is observable too.