It's 9:47 AM, your phone buzzes with a "the dashboard feels slow" message from the customer success team, and you have about three minutes before someone in product asks the same question in a different channel. You don't have time to spin up a tracing tool you've never used. You need a single page that says "yes, here's what's slow, here's how slow it has been for the last hour, here's the SQL behind it."

That's the question Pulse was built to answer. Released in April 2024, it's Laravel's first-party in-app monitoring dashboard. It runs alongside your application, records lightweight samples of every request and queue job, and shows you the result on /pulse without you needing a separate Datadog account.

What Pulse Actually Is

Pulse is a composer package that adds a /pulse route, a set of "recorders" that hook into the framework's events, and a configurable storage backend. Install:

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

After install you have a dashboard at /pulse, gated by a Gate::define('viewPulse', ...) you set up in your service provider. The dashboard is composed of cards. Each card reads from pulse_* tables that the recorders write to.

Built-in cards in the default layout:

  • Application Usage — top users by request count, top routes, top dispatchers of jobs.
  • Slow Requests — requests whose duration exceeds a configurable threshold (1,000ms by default).
  • Slow Jobs — same idea, queue side.
  • Slow Queries — SQL statements taking longer than the threshold, with the call site.
  • Slow Outgoing Requests — Guzzle calls that took too long, grouped by host.
  • Cache — hit/miss ratios per cache key prefix.
  • Exceptions — recent thrown exceptions, grouped by class and location.
  • Queues — depth and throughput per queue (similar to Horizon, smaller surface).
  • Servers — CPU, memory, disk for each host running Pulse's pulse:check daemon.

You don't have to use all of them. The dashboard layout lives in a Livewire component you can copy into your app and edit — drop the cards you don't care about, add custom ones, reorder, resize. That's the part most teams miss: Pulse is opinionated about what to record, not about how to display it.

The Sampling Knob That Matters

Every request that Pulse touches writes a few rows. On a low-traffic app this is invisible. On a high-traffic app it can quietly become your second-largest source of database write volume.

The lever is sample_rate in config/pulse.php, configurable per recorder. The default is 1 (record everything). Set it to 0.1 and Pulse keeps statistical fidelity by extrapolating from 10% of requests:

PHP
'recorders' => [
    Recorders\UserRequests::class => [
        'enabled' => env('PULSE_USER_REQUESTS_ENABLED', true),
        'sample_rate' => env('PULSE_USER_REQUESTS_SAMPLE_RATE', 1),
        'ignore' => [
            '#^/horizon#',
            '#^/pulse#',
            '#^/health#',
        ],
    ],
    Recorders\SlowRequests::class => [
        'sample_rate' => 1,
        'threshold' => 1000,
        'ignore' => [
            '#^/horizon#',
            '#^/pulse#',
        ],
    ],
],

Note the asymmetry. Slow-request recording stays at sample_rate: 1 — the events are rare and the rare ones are exactly what you want — while high-volume recorders like cache hits or user requests can drop to a tenth and lose nothing useful. Most teams that complain about Pulse "slowing the app down" left every recorder at default rate on a 5,000-RPS API.

The other lever is storage. Pulse's first-party storage requires MySQL, MariaDB, or PostgreSQL — there's no SQLite-backed mode. Production-shape configurations:

  • Same database, separate tables — fine for low-volume apps, simplest setup.
  • Separate MySQL/Postgres database — point PULSE_DB_CONNECTION at its own connection so Pulse writes don't contend with your app's writes.
  • Redis ingest in front of the DB — set PULSE_INGEST_DRIVER=redis and Pulse buffers entries in a Redis stream. A pulse:work daemon consumes the stream and writes to the database. That smooths out the write rate on a high-traffic app and keeps individual requests cheap.

Whiteboard sketch of how Pulse plugs into a Laravel request: HTTP request enters the kernel, framework events fan out to SlowRequests, SlowQueries, Exceptions and a custom stripe_webhook recorder, each writing samples through an optional Redis ingest stream into pulse_* tables, with the /pulse Livewire dashboard reading aggregates back out, and a side note on which recorders to leave at sample_rate: 1 versus drop to 0.1.
Recorders sit on framework events, not in your request handlers — that's why sampling is the lever and storage is the cost.

What The Slow Queries Card Actually Tells You

The single card most worth setting up correctly is Slow Queries. It captures the SQL string, the configured threshold (default 1000ms), and — critically — the file and line number of the PHP code that issued the query.

Text
SELECT * FROM orders WHERE status = ? AND created_at >= ?
  app/Http/Controllers/DashboardController.php:42
  Avg: 1.8s · Count: 2,340 · 1h

That last line is what turns a graph into a fix. You're not staring at "some SELECT is slow" — you're staring at the controller method that issued it 2,340 times in the last hour. From there it's usually one of three things: missing index, N+1, or a query that needs a redesign. All three are answerable in minutes once you have the call site.

A small operational note: enabling slow-query capture pulls SQL strings into your Pulse database. If those queries embed sensitive parameters, you have a privacy concern. Pulse strips bound parameters by default — verify in your environment, and use the ignore config to exclude routes or queries you don't want stored.

Mock-up of the Pulse dashboard at /pulse: top row of cards for Slow Requests (above the configured threshold), Slow Queries (sorted by total impact, with file:line), Slow Jobs, and Cache hit/miss ratio. Below them, an Exceptions card listing recent thrown exception classes with counts and the route they came from, and a Servers card with CPU/memory/disk gauges for two hosts. Right column shows Application Usage with top users, top routes, top job dispatchers.
What Pulse looks like at 9:47 AM when someone tells you the dashboard feels slow.

The Servers Card Has A Quiet Requirement

Servers shows live CPU, memory, and disk per host. To populate it, each host needs a running php artisan pulse:check daemon. It's a long-running process — same shape as Horizon, same supervisor pattern.

INI
[program:pulse-check]
process_name=%(program_name)s
command=php /var/www/app/artisan pulse:check
autostart=true
autorestart=true
user=www-data

Forget this and the card sits empty. It's the most common "Pulse doesn't work for me" report. The web server alone cannot record server stats — there's no request hook for "ambient CPU usage when nobody is hitting an endpoint."

Custom Cards For Domain Metrics

The recorder API is the part that turns Pulse from a generic monitor into something useful for your specific app. You record arbitrary samples with Pulse::record():

PHP
use Laravel\Pulse\Facades\Pulse;

class StripeWebhookController
{
    public function __invoke(Request $request)
    {
        $event = StripeEvent::constructFrom($request->all());

        $startedAt = $request->server('REQUEST_TIME_FLOAT');
        $durationMs = $startedAt
            ? (microtime(true) - $startedAt) * 1000
            : 0;

        Pulse::record('stripe_webhook', $event->type, $durationMs)
            ->count()
            ->avg();

        ProcessStripeWebhook::dispatch($event);

        return response()->noContent();
    }
}

Pulse::record() takes three arguments — the type, the key the rows should aggregate by, and the value to aggregate. count() and avg() are aggregation methods you chain on; avg() averages whatever value you handed to record().

Then a thin Livewire card reads from the pulse_aggregates table for that key and renders the breakdown. Now you have a "Stripe events processed in the last hour" card next to your slow queries — same dashboard, same place you check at 9:47 AM, same sampling and storage configuration.

The trap with custom recorders is forgetting they go through Pulse's storage. A high-cardinality key (Pulse::record('user_action', $userId) with millions of users) will produce millions of rows. Aggregate before recording — record per type, not per id — unless you genuinely need per-user breakdowns.

Pulse vs Horizon vs Telescope vs External APM

These four tools overlap, and teams sometimes install all of them and use none well. The split that holds up:

  • Telescope is for development. It records every query, every request, every event in a way that would crush production. Don't ship it.
  • Horizon is for queues. Throughput, wait time, supervisors, retry/forget for failed jobs. Pulse can show queue depth but Horizon goes deeper.
  • Pulse is the in-app dashboard for "is anything slow right now?" — the on-call view. Cheap, lives next to the app, gives you call sites for slow queries.
  • External APM (Sentry, Datadog, New Relic) is for the day your database is on fire. Pulse stores its data in the same infrastructure as the app it's watching — fine 99% of the time, useless when that infrastructure is the problem. Sentry plus Pulse is a solid pair: Sentry for exceptions and incident telemetry, Pulse for the sub-second "what does the app feel like" view.

The mistake is treating Pulse as a Datadog replacement. It isn't — it's a great in-process companion to one. Pair them; don't replace one with the other.

Four-column comparison of Telescope, Horizon, Pulse and external APM tools, each panel listing what it records, where it stores data, when to reach for it, its limits, and the persona that uses it — Telescope for the developer at the IDE, Horizon for queue-incident on-call, Pulse for the 9:47 AM "is the app slow now?" page, and external APM for the SRE running a postmortem.
The four tools answer different questions — Pulse is the only one that lives next to the code that broke.

Worth Installing Early

Unlike most observability tools, Pulse is small enough to install on day one of a project without future regret. The recorders are off-by-default for several cards, the storage is local, and the dashboard is genuinely useful even on a side project. You see your own slow endpoints before users do, you see N+1 queries the first time you exercise a list view, and you get a /pulse page that's already wired up the day a customer says "the app feels slow."

That last benefit is the underrated one. The cost of installing Pulse during an incident is much higher than installing it on a quiet Tuesday. Do the latter.

A One-Sentence Mental Model

Pulse is an in-app, opinionated, sampled dashboard for Laravel — recorders sit on framework events, write samples to a small set of tables, and a Livewire dashboard at /pulse shows you slow requests, slow queries with their call sites, queue depth, and any custom metrics you record yourself, which is enough to answer "what's slow right now" without leaving the framework.