Slow queries, memory hogs, failed jobs, cache misses, exception spikes, and the exact users hitting errors — Pulse is the production monitoring tool baked into Laravel 11+ that most teams install, glance at once, and never configure properly. Here’s how to actually use it.
Most teams install Pulse, see the dashboard, think “cool,” and move on. The default cards are visible but not configured. Alerts don’t exist. Nobody checks it until something breaks. By then, Pulse has the history of what went wrong — but nobody thought to look.
Pulse is not just a vanity dashboard. It’s a production observability tool that, properly configured, answers questions like: “Which of our 50 endpoints is slow under load?” “Which user generates the most database queries?” “Are cache hit rates dropping when we deploy?” “How long before a server runs out of memory at the current growth rate?”
This post covers the complete Pulse setup — from installation to custom recorders to the patterns that make it genuinely useful in production.
Pulse vs Telescope vs Horizon — The Right Tool for Each Job
Before configuration: understanding what Pulse is and isn’t.
Telescope answers: "What exact SQL query did request #48291 execute?
What was the full stack trace for this exception?"
→ Deep introspection. Development + staging.
→ High storage cost — records everything.
Horizon answers: "How many jobs are pending? Should I scale workers?
What's my job throughput per minute?"
→ Queue management. Production.
→ Redis-specific.
Pulse answers: "What is the overall health of my app right now?
Which endpoints, queries, and users are causing problems?
Is my server about to run out of memory?"
→ Aggregate monitoring. Production.
→ Low overhead — uses sampling, not full recording.
The right answer: run all three. Telescope in local and staging (too verbose for production), Horizon for queue management, Pulse for production health monitoring. They complement rather than replace each other.
Installation
composer require laravel/pulse
php artisan vendor:publish --provider="Laravel\Pulse\PulseServiceProvider"
php artisan migrate
Pulse requires MySQL, MariaDB, or PostgreSQL for storage. SQLite is not supported (Pulse uses aggregate queries that require these databases).
Using a Separate Database for Pulse
In high-traffic applications, Pulse storage should live on a separate database connection to avoid affecting your application’s primary database:
// config/pulse.php
'storage' => [
'driver' => 'database',
'connection' => env('PULSE_DB_CONNECTION', 'pulse'),
],
// config/database.php — dedicated Pulse connection
'connections' => [
'pulse' => [
'driver' => 'mysql',
'host' => env('PULSE_DB_HOST', '127.0.0.1'),
'database' => env('PULSE_DB_DATABASE', 'pulse'),
'username' => env('PULSE_DB_USERNAME'),
'password' => env('PULSE_DB_PASSWORD'),
],
],
# Run Pulse migrations on the dedicated connection
php artisan migrate --database=pulse
Securing the Dashboard
By default, Pulse’s dashboard at /pulse is only accessible in local environments. In production and staging, define an authorization gate:
// app/Providers/PulseServiceProvider.php
// Or in AppServiceProvider if you prefer
use Laravel\Pulse\Facades\Pulse;
public function boot(): void
{
Pulse::auth(function (Request $request): bool {
// Only admins can access Pulse in production
return $request->user()?->hasRole('admin') ?? false;
// Or restrict by email
return in_array($request->user()?->email, [
'you@yourapp.com',
'teammate@yourapp.com',
]);
// Or allow all authenticated users in staging
// return app()->isLocal() || $request->user() !== null;
});
}
The Built-In Cards and What They Actually Tell You
1. Slow Requests — Your Performance Map
The Slow Requests card shows endpoints that take longer than a configured threshold.
// config/pulse.php
'recorders' => [
\Laravel\Pulse\Recorders\Requests::class => [
'enabled' => env('PULSE_REQUESTS_ENABLED', true),
'threshold' => env('PULSE_REQUESTS_SLOW', 1000), // ms — highlight requests over 1 second
'ignore' => [
'#^/pulse$#', // don't record Pulse's own dashboard requests
'#^/telescope#',
'#^/horizon#',
'#^/health$#',
],
],
],
What to look for: endpoints appearing in this list that weren’t there yesterday. Sudden appearances indicate a deployment regression. Gradual growth indicates a query that’s getting slower as the dataset grows.
2. Slow Queries — The Database Performance Heatmap
'recorders' => [
\Laravel\Pulse\Recorders\SlowQueries::class => [
'enabled' => env('PULSE_SLOW_QUERIES_ENABLED', true),
'threshold' => env('PULSE_SLOW_QUERIES', 1000), // ms
'ignore' => [
// Ignore known slow queries that are acceptable (backups, reports)
'#INFORMATION_SCHEMA#',
],
],
],
The slow queries card shows the query, the execution time, and how frequently it appears. When you see the same query appearing 500 times in an hour, you have either an N+1 problem or a query that needs an index.
3. Exceptions — Frequency and Trend
'recorders' => [
\Laravel\Pulse\Recorders\Exceptions::class => [
'enabled' => env('PULSE_EXCEPTIONS_ENABLED', true),
'ignore' => [
// Ignore expected exceptions that aren't bugs
\Illuminate\Auth\AuthenticationException::class,
\Illuminate\Validation\ValidationException::class,
\Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class,
],
],
],
The exceptions card shows exception counts over time. What’s useful isn’t just the count — it’s the trend. An exception that appears 5 times per hour is different from one that appears 5 times and then stops. Spikes after a deployment tell you exactly which deploy introduced the bug.
4. Queue Health
'recorders' => [
\Laravel\Pulse\Recorders\Queues::class => [
'enabled' => env('PULSE_QUEUES_ENABLED', true),
'threshold' => env('PULSE_QUEUES_SLOW', 1000), // highlight slow jobs
],
],
Pulse shows queue throughput (jobs per minute), slow jobs, and failed job counts. This overlaps with Horizon but is useful for teams not using Horizon, and provides a unified view alongside the other health metrics.
5. Usage — Who Is Using What
The Usage card is one of the most underappreciated features. It shows the top users by request count, slow request count, and exception count:
'recorders' => [
\Laravel\Pulse\Recorders\UserRequests::class => ['enabled' => true],
\Laravel\Pulse\Recorders\UserJobs::class => ['enabled' => true],
],
What this tells you:
- Top requests: who your power users are — and whether they’re generating more load than expected
- Top slow requests: which users consistently hit slow endpoints — might indicate a dataset problem specific to their account
- Top exceptions: which users are hitting the most bugs — the first people to contact when investigating a new exception type
6. Cache — Hit Rate Monitoring
'recorders' => [
\Laravel\Pulse\Recorders\CacheInteractions::class => [
'enabled' => env('PULSE_CACHE_ENABLED', true),
'ignore' => [
// Ignore internal framework cache keys
'#^illuminate:#',
'#^laravel:#',
'#^livewire:#',
],
],
],
The cache card shows hit vs miss ratios per cache key. A cache that’s supposed to be warm but shows 90% miss rate tells you the cache isn’t being populated correctly. A cache hit rate that drops after a deployment tells you the new code isn’t caching where the old code did.
7. Servers — Hardware Health
The servers card requires a daemon running on each server:
# Run on every server you want to monitor
php artisan pulse:check
# Via Supervisor — run continuously
process_name=%(program_name)s
command=php /var/www/app/artisan pulse:check
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/pulse-check.log
The servers card shows CPU usage, memory usage, and storage usage per server. For teams running multiple servers or load-balanced deployments, this gives a unified view of all server health in one dashboard.
Ingest Configuration: High-Traffic Apps
By default, Pulse records synchronously — every request writes directly to the Pulse database. On high-traffic applications, this adds latency to every request.
Redis Ingest for High Traffic
// config/pulse.php
'ingest' => [
'driver' => env('PULSE_INGEST_DRIVER', 'redis'),
'redis' => [
'connection' => env('PULSE_REDIS_CONNECTION', 'default'),
'chunk' => 1000,
],
],
# .env
PULSE_INGEST_DRIVER=redis
With Redis ingest, Pulse writes to Redis (fast, non-blocking) and a separate worker processes the Redis data into the Pulse database:
# Worker that drains Redis ingest queue to Pulse DB
php artisan pulse:work
# Via Supervisor
[program:pulse-work]
command=php /var/www/app/artisan pulse:work
autostart=true
autorestart=true
user=www-data
Sampling — Record a Percentage of Requests
For very high-traffic applications, recording every request is unnecessary:
// config/pulse.php
'recorders' => [
\Laravel\Pulse\Recorders\Requests::class => [
'enabled' => true,
'sample_rate' => env('PULSE_SAMPLE_RATE', 0.1), // record 10% of requests
],
],
At 10% sampling, a server processing 10,000 requests per minute records 1,000. The aggregate metrics remain statistically accurate.
Data Retention
// config/pulse.php
'ingest' => [
'trim' => [
'lottery' => [1, 1000], // trim old data 1 in 1000 requests
'keep' => '7 days', // keep 7 days of data
],
],
For production, 7 days is usually sufficient for performance investigations. If you need longer history, increase this — but be aware the Pulse tables will grow significantly.
Building Custom Recorders: Track Your Business Metrics
The built-in cards cover infrastructure concerns. Custom recorders extend Pulse to cover your application’s specific business and operational metrics.
Architecture: Recorder + Card
A custom Pulse integration has two parts:
- Recorder — listens for events or polls for data, stores metrics
- Card — a Livewire component that queries and displays the stored metrics
Example: Subscription Revenue Recorder
// app/Pulse/Recorders/SubscriptionRevenue.php
<?php
namespace App\Pulse\Recorders;
use App\Events\SubscriptionRenewed;
use Laravel\Pulse\Facades\Pulse;
class SubscriptionRevenue
{
// The event this recorder listens to
public string $listen = SubscriptionRenewed::class;
public function record(SubscriptionRenewed $event): void
{
Pulse::record(
type: 'subscription_revenue',
key: $event->subscription->plan, // track per plan
value: $event->subscription->amount,
)->sum();
// Stores the sum of revenue per plan for aggregation
}
}
Example: API Response Time Tracker
// app/Pulse/Recorders/ExternalApiLatency.php
<?php
namespace App\Pulse\Recorders;
use Illuminate\Events\SharedBeat;
use Illuminate\Support\Facades\DB;
use Laravel\Pulse\Facades\Pulse;
class ExternalApiLatency
{
// SharedBeat fires on a schedule — used for polling-based metrics
public string $listen = SharedBeat::class;
private int $lastRecordedAt = 0;
public function record(SharedBeat $event): void
{
// Only run every 60 seconds
if ($event->time->timestamp - $this->lastRecordedAt < 60) {
return;
}
$this->lastRecordedAt = $event->time->timestamp;
// Measure external API latency
$start = microtime(true);
$status = 'ok';
try {
app(ExternalApiClient::class)->ping();
} catch (\Exception) {
$status = 'error';
}
$latency = round((microtime(true) - $start) * 1000);
Pulse::set('external_api_latency', 'stripe', $latency);
Pulse::set('external_api_status', 'stripe', $status);
}
}
Registering Custom Recorders
// config/pulse.php
'recorders' => [
// Built-in recorders...
\Laravel\Pulse\Recorders\Exceptions::class => [],
\Laravel\Pulse\Recorders\Requests::class => [],
// Your custom recorders
\App\Pulse\Recorders\SubscriptionRevenue::class => [],
\App\Pulse\Recorders\ExternalApiLatency::class => [],
],
The Custom Card Component
// app/Livewire/Pulse/SubscriptionRevenueCard.php
<?php
namespace App\Livewire\Pulse;
use Laravel\Pulse\Livewire\Card;
use Livewire\Attributes\Lazy;
#[Lazy]
class SubscriptionRevenueCard extends Card
{
public function render(): \Illuminate\View\View
{
$revenue = $this->pulse->aggregate(
type: 'subscription_revenue',
aggregate: 'sum',
interval: now()->subHours($this->periodAsHours()),
);
return view('livewire.pulse.subscription-revenue', [
'revenue' => $revenue,
'period' => $this->periodAsHours(),
]);
}
}
{{-- resources/views/livewire/pulse/subscription-revenue.blade.php --}}
<x-pulse::card :cols="$cols" :rows="$rows" :class="$class">
<x-pulse::card-header name="Subscription Revenue">
<x-slot:icon>💰</x-slot:icon>
</x-pulse::card-header>
<x-pulse::scroll :expand="$expand">
@foreach ($revenue as $plan => $amount)
<div class="flex justify-between items-center py-2">
<span class="text-sm font-medium">{{ $plan }}</span>
<span class="text-sm text-gray-600">₹{{ number_format($amount / 100, 2) }}</span>
</div>
@endforeach
</x-pulse::scroll>
</x-pulse::card>
Adding Cards to the Dashboard
{{-- resources/views/vendor/pulse/dashboard.blade.php --}}
<x-pulse>
{{-- Built-in cards --}}
<livewire:pulse.servers cols="full" />
<livewire:pulse.usage cols="4" rows="2" />
<livewire:pulse.queues cols="4" />
<livewire:pulse.cache cols="4" />
<livewire:pulse.slow-requests cols="8" />
<livewire:pulse.requests cols="4" />
<livewire:pulse.slow-queries cols="8" />
<livewire:pulse.exceptions cols="6" />
{{-- Custom cards --}}
<livewire:pulse.subscription-revenue-card cols="4" />
<livewire:pulse.external-api-latency cols="4" />
</x-pulse>
The Outdated Dependencies Recorder
One of the most useful custom recorders you can build: daily check for outdated Composer packages. Security vulnerabilities in dependencies are found daily. Pulse makes them visible:
// app/Pulse/Recorders/OutdatedDependencies.php
<?php
namespace App\Pulse\Recorders;
use Illuminate\Events\SharedBeat;
use Laravel\Pulse\Facades\Pulse;
class OutdatedDependencies
{
public string $listen = SharedBeat::class;
public function record(SharedBeat $event): void
{
// Only run at 2am to avoid overhead during business hours
if ($event->time->hour !== 2 || $event->time->minute !== 0 || $event->time->second !== 0) {
return;
}
$output = shell_exec('cd ' . base_path() . ' && composer outdated -D --format=json 2>/dev/null');
$packages = json_decode($output, true)['installed'] ?? [];
Pulse::set('outdated_deps', 'count', count($packages));
Pulse::set('outdated_deps', 'packages', json_encode(
collect($packages)->map(fn($p) => [
'name' => $p['name'],
'current' => $p['version'],
'latest' => $p['latest'],
])->all()
));
}
}
The Pulse Dashboard Configuration Checklist
Before considering Pulse “properly set up”:
Installation:
✓ Pulse installed and migrated
✓ Dedicated database connection for Pulse (not sharing the app database)
✓ Dashboard accessible at /pulse
✓ Auth gate restricts access to admins only
Recorders configured:
✓ Slow request threshold set (1000ms is a reasonable default)
✓ Internal paths ignored (pulse, telescope, horizon, health)
✓ Slow query threshold set (1000ms)
✓ Exception recorder excludes expected exceptions (404, validation, auth)
✓ Cache recorder ignores framework internal keys
Performance:
✓ Redis ingest driver enabled for high-traffic apps
✓ pulse:work command running via Supervisor (if Redis ingest is enabled)
✓ pulse:check command running via Supervisor on every monitored server
✓ Sample rate configured (0.1 for very high traffic, 1.0 for normal traffic)
✓ Data retention set to appropriate period (7 days is standard)
Dashboard layout:
✓ Servers card at the top (first thing to see is server health)
✓ Exceptions and Slow Requests visible without scrolling
✓ Usage card configured to show user-level data
✓ Custom cards added for business-specific metrics
Maintenance:
✓ pulse:prune scheduled in console routes (runs daily to clean old data)
Scheduling Pulse Pruning
// routes/console.php or app/Console/Kernel.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('pulse:prune')->daily();
Reading the Dashboard: What to Look for Daily
A 2-minute daily Pulse review catches most problems before users report them:
1. Servers card — CPU or memory trending up?
Normal: CPU spikes then returns to baseline
Problem: CPU or memory consistently climbing without returning
2. Exceptions card — any new exceptions since yesterday?
Normal: same exceptions at roughly the same rate
Problem: new exception type, or existing exception rate suddenly up
3. Slow Requests — any new endpoints in the list?
Normal: same slow endpoints as always (investigate and fix these)
Problem: new endpoints appearing — regression from recent deployment
4. Usage card — any unusual user showing at the top?
Normal: your power users and internal tools
Problem: an unknown user making hundreds of requests — potential scraping
or a specific user account with a problem causing loops
5. Cache card — hit rate stable?
Normal: consistent hit rate for your warm caches
Problem: hit rate drop after deployment — caching logic changed
The Relationship to Telescope: When to Use Each
Question: "Which exact SQL was run for this specific request?"
→ Telescope (you need per-request detail)
Question: "Which SQL query appears most often in slow requests?"
→ Pulse (you need aggregate frequency)
Question: "Why is this specific job failing?"
→ Telescope (full exception + payload)
Question: "How many jobs are failing per hour, and is it trending up?"
→ Pulse (you need trend data)
Question: "What did this specific user do before the error?"
→ Telescope (request timeline)
Question: "Which users generate the most exceptions?"
→ Pulse (aggregate by user)
The combination: Pulse tells you there’s a problem and roughly where it is. Telescope tells you the exact details. Use Pulse in production always, Telescope in production sparingly (high overhead, only when investigating a specific issue).
Final Thoughts
Pulse is one of the best additions to the Laravel ecosystem in recent years — not because it does anything that wasn’t possible before, but because it makes production visibility genuinely easy. Installation is minutes. The built-in cards cover the most common production concerns. Custom recorders let you track anything your business cares about.
The teams that get the most value from Pulse are the ones who treat the dashboard as a daily tool rather than an emergency tool. Two minutes every morning. Look for exceptions you don’t recognise. Look for slow endpoints that appeared overnight. Look at server memory trends before they become emergencies.
The dashboard that knows more about your app than you do is only useful if you look at it. Configure it properly, open it daily, and it becomes the first place you go when something seems wrong — rather than the last.
