Laravel Telescope: The Debugging Superpower That Most Developers Install and Never Actually Use

Requests, queries, jobs, mail, cache hits, exceptions, dumps — Telescope captures everything your app does in real time. Here’s how to go beyond “it’s installed” and use it to find bugs in minutes that would otherwise take hours.


Most Laravel developers have a pattern with Telescope: they install it during setup, visit /telescope once to confirm it works, and then never open it again — reaching for dd() and Log::info() every time something breaks.

This is like buying a Formula 1 car and using it to pick up groceries.

Telescope is the most complete real-time introspection tool available for Laravel. Every HTTP request, every database query, every queued job, every exception, every cache operation, every mail message, every scheduled task — Telescope captures all of it with full context, a rich UI, and filtering capabilities that make finding the problem a matter of seconds rather than minutes.

The developers who use Telescope properly spend significantly less time debugging. This post shows you what “properly” looks like.


Installation and Configuration

composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate

The Critical Configuration Decision: What to Watch

By default, Telescope records everything. On a busy application, this fills the database quickly and introduces overhead. The first configuration task is deciding what Telescope should capture.

// config/telescope.php
return [
    // Only enable in local and staging environments
    'enabled' => env('TELESCOPE_ENABLED', true),

    // Storage driver — database is default, but you can use custom drivers
    'driver' => env('TELESCOPE_DRIVER', 'database'),

    // How long to keep entries (hours)
    'storage' => [
        'database' => [
            'connection' => env('DB_CONNECTION', 'mysql'),
            'chunk'      => 1000,
        ],
    ],

    // Prune entries older than this many hours
    'prune' => [
        'hours'       => 24,    // keep 24 hours of entries
        'keep_exceptions' => 0, // keep exceptions indefinitely (0 = no limit)
    ],
]

Watchers — Enable Only What You Need

// config/telescope.php — the watchers section
'watchers' => [
    Watchers\BatchWatcher::class         => env('TELESCOPE_BATCH_WATCHER',         true),
    Watchers\CacheWatcher::class         => env('TELESCOPE_CACHE_WATCHER',         true),
    Watchers\CommandWatcher::class       => env('TELESCOPE_COMMAND_WATCHER',       true),
    Watchers\DumpWatcher::class          => env('TELESCOPE_DUMP_WATCHER',          true),
    Watchers\EventWatcher::class         => env('TELESCOPE_EVENT_WATCHER',         false), // noisy — disable unless debugging events
    Watchers\ExceptionWatcher::class     => env('TELESCOPE_EXCEPTION_WATCHER',     true),
    Watchers\GateWatcher::class          => env('TELESCOPE_GATE_WATCHER',          false), // disable unless debugging auth
    Watchers\JobWatcher::class           => env('TELESCOPE_JOB_WATCHER',           true),
    Watchers\LogWatcher::class           => env('TELESCOPE_LOG_WATCHER',           true),
    Watchers\MailWatcher::class          => env('TELESCOPE_MAIL_WATCHER',          true),
    Watchers\ModelWatcher::class         => env('TELESCOPE_MODEL_WATCHER',         false), // very noisy — enable only when needed
    Watchers\NotificationWatcher::class  => env('TELESCOPE_NOTIFICATION_WATCHER',  true),
    Watchers\QueryWatcher::class => [
        'enabled'  => env('TELESCOPE_QUERY_WATCHER', true),
        'slow'     => 50,  // highlight queries slower than 50ms
    ],
    Watchers\RedisWatcher::class         => env('TELESCOPE_REDIS_WATCHER',         false),
    Watchers\RequestWatcher::class => [
        'enabled'          => env('TELESCOPE_REQUEST_WATCHER', true),
        'size_limit'       => env('TELESCOPE_RESPONSE_SIZE_LIMIT', 64),  // KB — limit recorded response body size
    ],
    Watchers\ScheduleWatcher::class      => env('TELESCOPE_SCHEDULE_WATCHER',      true),
    Watchers\ViewWatcher::class          => env('TELESCOPE_VIEW_WATCHER',          false), // disable unless debugging views
],

Filtering What Gets Recorded

Telescope’s filtering capability is one of its most valuable features. You can tell it to only record entries that match specific conditions:

// app/Providers/TelescopeServiceProvider.php
use Laravel\Telescope\IncomingEntry;
use Laravel\Telescope\Telescope;

public function register(): void
{
    // Only record data when the request comes from specific IPs
    // (your dev team's IPs in staging, for example)
    Telescope::filter(function (IncomingEntry $entry) {
        if ($this->app->isLocal()) {
            return true;  // record everything locally
        }

        // In staging: only record for specific users or IPs
        return $entry->isReportableException() ||
               $entry->isFailedJob()            ||
               $entry->isScheduledTask()        ||
               $entry->hasMonitoredTag();
    });
}

Ignoring Noise — Specific Paths and Commands

// app/Providers/TelescopeServiceProvider.php
public function register(): void
{
    Telescope::night();  // enable dark mode in the UI

    // Ignore health check endpoints — they'd flood the requests tab
    Telescope::ignorePaths([
        'horizon/api/*',
        'telescope/api/*',
        'nova-api/*',
        '_debugbar/*',
        'health',
        'up',
    ]);

    // Ignore scheduled commands that run frequently and aren't interesting
    Telescope::ignoreCommands([
        'schedule:run',
        'telescope:prune',
        'horizon:snapshot',
        'queue:monitor',
    ]);
}

Protecting the Dashboard in Staging

// app/Providers/TelescopeServiceProvider.php
use Laravel\Telescope\Telescope;

public function boot(): void
{
    $this->gate();
}

protected function gate(): void
{
    Gate::define('viewTelescope', function ($user) {
        return in_array($user->email, [
            'admin@yourapp.com',
            'dev@yourapp.com',
        ]) || $user->hasRole('developer');
    });
}

The Requests Watcher: Your Application’s Traffic Log

The Requests tab is the most valuable starting point for any debugging session. Every HTTP request is recorded with:

  • Full URL, method, and response code
  • Request headers and body (sanitised)
  • Response body (up to the configured size limit)
  • Duration
  • Memory usage
  • Every query fired during the request (linked to the Queries tab)
  • Every exception thrown (linked to Exceptions)

Debugging a “Why is this request slow?” Problem

Workflow:
1. Open /telescope → Requests
2. Find the slow request (sorted by duration — click column header)
3. Click the request → see the breakdown
4. Check the "Queries" section in the request detail — how many queries? How slow?
5. Check the "Duration" — is most time in queries or PHP execution?
6. If queries: click through to the Queries tab, find the slow or duplicated ones
7. If PHP execution: add Telescope::dump() markers to narrow down

Reading the Request Detail

The request detail page shows everything about a single request in one place:

Request: POST /api/orders
Status:  201
Duration: 847ms
Memory:   42MB

Payload:
{
  "items": [{"id": 1, "qty": 2}],
  "coupon_code": "SAVE10"
}

Response: { "id": 123, "status": "pending", "total": 1800 }

Queries (47):                    ← 47 queries for one request is your N+1
  SELECT * FROM orders WHERE...  → 2ms
  SELECT * FROM products WHERE id = 1  → 1ms   ← these are repeating
  SELECT * FROM products WHERE id = 1  → 1ms   ← same query 12 times
  SELECT * FROM users WHERE id = 5     → 1ms   ← this one 3 times
  ...

Exceptions:
  (none)

A request showing 47 queries immediately tells you there’s an N+1 problem. You don’t need to guess, add log statements, or reproduce the issue in a test — Telescope shows you the evidence directly.


The Queries Watcher: Finding N+1 and Slow Queries

The Queries tab is where performance bugs become undeniable.

What Each Query Entry Shows

Every query entry shows:

  • The full SQL with bound values substituted in
  • Execution time
  • The connection name
  • The file and line number that triggered the query

The line number is the most valuable part. You don’t have to grep for Eloquent calls — Telescope tells you exactly which line of application code caused each query.

The Slow Query Threshold

Configure the threshold in your watcher settings — anything above it is highlighted in red:

Watchers\QueryWatcher::class => [
    'enabled' => env('TELESCOPE_QUERY_WATCHER', true),
    'slow'    => 50,  // queries taking > 50ms are highlighted red
],

Any query highlighted red in Telescope is a query you should examine. Check for missing indexes, unnecessary joins, or data fetching that should be moved to a cache.

The Duplicate Query Pattern

When you see the same query appearing multiple times with different WHERE id = ? values, that’s your N+1 pattern in Telescope. The fix is with() eager loading — and the query tab shows you exactly which relationship is being loaded lazily.

Using Telescope::tag() for Query Context

Add tags to requests or jobs to make filtering easier:

// In a ServiceProvider or middleware — tag requests by user role
Telescope::tag(function (IncomingEntry $entry) {
    $tags = [];

    if (auth()->check()) {
        $tags[] = 'user:' . auth()->id();
        $tags[] = 'role:' . auth()->user()->role;
    }

    return $tags;
});
// In a job — tag for filtering in Telescope
class ProcessOrder implements ShouldQueue
{
    use InteractsWithTelescope;

    public function tags(): array
    {
        return ['order:' . $this->order->id, 'user:' . $this->order->user_id];
    }
}

With tags, you can search the Queries tab for user:42 and see every query fired for that specific user across all their requests.


The Jobs Watcher: Debugging Background Work

Background jobs are notoriously hard to debug. They run asynchronously, they don’t appear in HTTP request logs, and their failures are often silent. Telescope makes them visible.

What Job Entries Show

Every job entry shows:

  • Job class name and queue name
  • Status (pending, completed, failed)
  • Duration and memory
  • Every query fired inside the job
  • Any exceptions thrown
  • The job payload (what data it was given)
  • Number of attempts

Debugging a Failed Job

Workflow:
1. Open /telescope → Jobs
2. Filter by status: "failed"
3. Click the failed job → see the exception and stack trace
4. See the exact job payload — what data caused the failure
5. See every query the job ran before failing
6. Use the exception stack trace to find exactly which line threw

This eliminates the “add log statements, re-queue the job, wait for it to process, read the logs” cycle. The full failure context is already in Telescope.

Watching Job Queues in Real Time

Filter by queue name to watch specific queues:
- Show only "emails" queue jobs
- Show only "failed" jobs across all queues
- Show jobs for a specific user with tags: user:42

The Exceptions Watcher: No More “Check the Logs”

Every unhandled exception is captured with its full stack trace, the request that triggered it, and the context values at the time of the exception.

What Exception Entries Show

  • Exception class and message
  • Full stack trace with file and line numbers
  • The HTTP request that triggered it (if applicable)
  • All context values — user, session, request data
  • Number of occurrences (Telescope deduplicates repeated exceptions)

The Occurrence Count

Telescope deduplicates exceptions — instead of seeing 500 identical “ModelNotFoundException” entries, you see one entry with an occurrence count of 500. This immediately tells you the scale of a problem:

Exceptions:
  App\Exceptions\ProductNotFoundException  — 1,247 occurrences today
  Illuminate\Auth\AuthenticationException  — 203 occurrences
  App\Exceptions\PaymentFailedException    — 12 occurrences

The first one — 1,247 occurrences — is a product that was deleted but is still being referenced somewhere. Telescope shows you exactly which request and which line triggered it.


The Mail Watcher: Preview Emails Without Actually Sending

The Mail watcher captures every email before it’s sent and shows it in Telescope — with a full preview of the HTML email body, the recipient, subject, and all headers.

This eliminates the need for Mailtrap or any external mail testing service in local development.

Using the Mail Preview

Workflow for testing an email:
1. Trigger the action that sends the email (register, order placed, etc.)
2. Open /telescope → Mail
3. Click the mail entry
4. See the full rendered HTML email — exactly what the recipient would receive
5. Check recipients, subject, from address, and CC/BCC
6. Click "Preview" to see the rendered email in your browser

Debugging Missing Emails

If a user reports “I never received my confirmation email”:

1. Open /telescope → Mail
2. Search by recipient email or subject
3. If you find the entry: the email was sent — check spam, check their email
4. If you don't find it: the code path that sends it was never executed
   — check the Jobs tab (is there a queued notification?)
   — check the Exceptions tab (did it throw before reaching the send?)

The Cache Watcher: Understanding Cache Hit Rates

The Cache watcher records every get, set, forget, hit, and miss operation. This is invaluable for understanding whether your cache is working as expected.

What Cache Entries Show

  • The cache key
  • The operation (hit, miss, set, forget)
  • The TTL (for set operations)
  • The value (truncated for large values)

Common Patterns to Look For

The miss followed by set: Expected — first request misses, populates the cache, subsequent requests hit.

Repeated misses with no set: Your cache is not being populated. Either the key is wrong, the TTL is too short, or the cache set is never executing.

Misses on data that should be cached: Suggests the cache key doesn’t match between the write and the read — often a case-sensitivity issue or a key that includes a variable that’s changing unexpectedly.

Cache entries for /api/products (one request):
  products:page:1  → HIT  (32ms)     ← this is good
  products:page:2  → MISS (142ms)    ← miss, then:
  products:page:2  → SET  (ttl: 300) ← populated correctly
  user:5:prefs     → MISS            ← should this be cached?
  user:5:prefs     → SET             ← it is now

The Dump Watcher: The Right Way to Use dump() in Development

Instead of dd() — which stops execution and breaks your request — use dump() or Telescope’s Telescope::dump(). These write to Telescope without interrupting the request flow.

// ✗ dd() — breaks the request and returns garbage HTML to your API consumer
dd($order->items);

// ✓ dump() — Telescope captures it, request continues normally
dump($order->items);

// ✓ Or use telescope() helper for more context
telescope('order-items', $order->items->toArray());

Dumps Appear in the Telescope Dumps Tab

Every dump() call appears in the Dumps tab with:

  • The dumped value (formatted, not raw PHP output)
  • The file and line number that called dump()
  • The timestamp

This means you can add debug dumps throughout your code, run a request, and see all the dump values in one place in the correct order — without hunting through terminal output or log files.


Advanced Technique: Watching Specific Users in Staging

One of Telescope’s most powerful production-adjacent uses is watching specific users in a staging environment. With tags and filtering, you can see everything that happens when a QA tester or a beta user interacts with the application.

// app/Providers/TelescopeServiceProvider.php
public function register(): void
{
    Telescope::filter(function (IncomingEntry $entry) {
        if ($this->app->isLocal()) {
            return true;
        }

        // In staging: only record entries for watched users
        $watchedUsers = config('telescope.watched_users', []);

        if (!empty($watchedUsers) && auth()->check()) {
            return in_array(auth()->user()->email, $watchedUsers);
        }

        // Always record exceptions and failed jobs regardless
        return $entry->isReportableException() || $entry->isFailedJob();
    });
}
# .env.staging
TELESCOPE_ENABLED=true
TELESCOPE_WATCHED_USERS=qa@yourapp.com,beta@yourclient.com

When your QA tester reports “the checkout is broken for me”, you open Telescope, filter by their email tag, and see exactly what happened — every request they made, every query those requests fired, and any exceptions that were thrown.


The Scheduled Tasks Watcher: Finally See What Your Cron Jobs Are Doing

The Schedule watcher captures every scheduled task execution with its output, duration, and whether it succeeded or failed.

Schedule entries:
  App\Console\Commands\SendDailyDigest  — 14:00  — completed  — 2.3s
  App\Console\Commands\SyncInventory    — 14:05  — completed  — 15.1s
  app:generate-reports                  — 14:10  — failed     — 0.2s  ← investigate
  telescope:prune                       — 14:15  — completed  — 0.1s

For the failed task, click through to see the exception and output. This is infinitely better than SSH-ing to the server, finding the cron log, and parsing timestamp-prefixed output lines.


The Debugging Workflow: A Real Example

Let’s walk through a real debugging session — a report that “the order confirmation email isn’t arriving for some users.”

Step 1: Open /telescope → Requests
        Filter: POST /api/orders
        Find: requests where status is 201 (order placed successfully)
        Pick a request from a user who didn't receive the email

Step 2: In the request detail
        Check "Notifications" — did a notification fire?
        Check "Mail" — was an email dispatched?
        Check "Jobs" — was a queued notification dispatched?

Step 3: If a job was dispatched
        Go to Jobs tab → find the job by timestamp or user tag
        Check status: did it complete? did it fail?

Step 4: If the job failed
        See the full exception in the job detail
        Read the stack trace — what line failed?
        See the job payload — what user/order data was it given?

Step 5: Fix the underlying issue
        The job payload might show the user's email_verified_at is null
        The sending logic might check this before sending
        Found: email only sends to verified users — user hadn't verified
        Fix: notify unverified users differently or at verification

This entire investigation — from “user didn’t receive email” to “found the root cause” — took under five minutes because Telescope had the full context already recorded. Without Telescope, this would have required adding log statements, replicating the test user’s exact state, triggering the flow again, and parsing log output.


Telescope Shortcuts Reference

The tabs you'll use most often and what to use each for:

Requests      Every HTTP request — start here for any user-reported bug
Queries       N+1 detection, slow query finding, missing indexes
Exceptions    Error frequency and full context — check daily in staging
Jobs          Background job debugging — failures, duration, payloads
Mail          Email preview and delivery debugging
Cache         Cache hit rate analysis, key debugging
Dumps         debug() and dump() output — use instead of dd()
Schedule      Scheduled task success/failure and output
Notifications Notification dispatch and channel routing

Keyboard shortcut: / in the Telescope UI focuses the search
                  R refreshes the current tab
                  Press a column header to sort

Production Usage: When and How

Telescope should not run in production by default — it stores data in the database and adds overhead to every request. But it can run selectively:

// .env.production — off by default
TELESCOPE_ENABLED=false

// Enable temporarily when investigating a specific issue:
// 1. Set TELESCOPE_ENABLED=true
// 2. Add a filter to only watch the affected user's email
// 3. Ask the user to reproduce the issue
// 4. Investigate immediately in Telescope
// 5. Set TELESCOPE_ENABLED=false again

For always-on production use, run Telescope only for exceptions and failed jobs (which are low volume but high value) and disable the high-volume watchers like RequestWatcher and QueryWatcher.


Keeping Telescope Clean: Pruning

Set up automatic pruning to prevent the telescope database tables from growing indefinitely:

// app/Console/Kernel.php
protected function schedule(Schedule $schedule): void
{
    // Prune entries older than 48 hours, run hourly
    $schedule->command('telescope:prune --hours=48')->hourly();
}
# Manual pruning
php artisan telescope:prune          # prune default (24 hours)
php artisan telescope:prune --hours=48  # keep 48 hours

Final Thoughts

Telescope is not a tool you visit once and forget. It’s the first tab you open when something breaks.

The developers who get the most value from it treat it as their application’s flight recorder — a complete record of everything that happened, available immediately, with full context. When a bug is reported, Telescope often shows you the cause before you’ve even thought about how to reproduce the issue.

The investment is small: configure your watchers, add tags to your jobs and requests, set up a filter for your staging environment. An afternoon of configuration that pays dividends on every debugging session for the life of the application.

The next time you reach for dd() or start adding Log::info() statements, open Telescope first. There’s a good chance the answer is already there.

Leave a Reply

Your email address will not be published. Required fields are marked *