Profiling Laravel with Blackfire and Xdebug: Finding Real Bottlenecks
#laravel #performance #blackfire #xdebug #profiling

Profiling Laravel with Blackfire and Xdebug: Finding Real Bottlenecks

3 min read Mohamed Said Mohamed Said

Why "It Feels Slow" Is Not a Debugging Strategy

Every Laravel app eventually develops a slow endpoint. The instinct is to add a cache layer or throw an index at the database. Sometimes that works. More often, you've optimised the wrong thing entirely. Profiling tools give you evidence before you act.

This article focuses on two complementary tools: Xdebug for local call-graph analysis and Blackfire for timeline-based profiling in staging or production-like environments.


Xdebug: Call-Graph Profiling Locally

Xdebug's profiler writes a Cachegrind file you can open in KCachegrind (Linux) or QCachegrind (macOS/Windows). Enable it per-request so you don't drown in noise.

; php.ini or xdebug.ini
xdebug.mode=profile
xdebug.output_dir=/tmp/xdebug
xdebug.profiler_output_name=cachegrind.out.%p.%r
xdebug.start_with_request=trigger

Trigger a profile by appending ?XDEBUG_PROFILE=1 to any request, or set the cookie XDEBUG_PROFILE=1 for a session.

curl "http://localhost/api/reports?XDEBUG_PROFILE=1" \
  -H "Authorization: Bearer $TOKEN"

Open the output file in QCachegrind and sort by Self Cost. You're looking for functions that consume time excluding their callees — those are the real culprits, not just the top of the call stack.

What to Look For

  • Illuminate\Database\Connection::select called hundreds of times → N+1 query
  • Illuminate\Container\Container::resolve dominating → expensive service resolution on every request
  • json_decode / serialize in a hot loop → unnecessary (de)serialisation

Blackfire: Timeline Profiling for Realistic Environments

Blackfire instruments PHP at the C extension level with near-zero overhead, making it safe for staging. Its timeline view maps wall-clock time across layers: PHP, SQL, HTTP calls, and cache.

Install the agent and probe, then trigger a profile from the CLI:

blackfire curl https://staging.example.com/api/reports \
  -H "Authorization: Bearer $TOKEN"

Or use the browser extension for authenticated sessions.

Reading the Call Graph

Blackfire's call graph shows inclusive time (a node + all descendants) and exclusive time (the node alone). Focus on nodes with high exclusive time and low call count first — they're doing expensive work in a single shot.

App\Http\Controllers\ReportController::index  42ms (excl: 0.3ms)
  └─ App\Services\ReportBuilder::compile       41ms (excl: 38ms)  ← HERE
       └─ Illuminate\Support\Collection::map    3ms

In this example, ReportBuilder::compile is doing 38 ms of work itself. Drilling in reveals it's calling array_map over a 10 000-row result set that should have been aggregated in SQL.


Practical Workflow: Blackfire Assertions in CI

Blackfire supports .blackfire.yaml assertions so slow regressions fail the build:

# .blackfire.yaml
tests:
  "Report endpoint stays fast":
    path: "/api/reports"
    assertions:
      - "main.wall_time < 200ms"
      - "metrics.sql.queries.count < 10"

Pair this with blackfire run php artisan test to profile your test suite's critical paths without a browser.


Combining Both Tools

Use Xdebug locally to explore unfamiliar code paths — the Cachegrind call graph is exhaustive. Switch to Blackfire when you need timeline context ("is this slow because of PHP or because of a slow query?") or when you want assertions in CI.

Neither tool replaces the other. Xdebug is a microscope; Blackfire is a dashboard.


Takeaways

  • Enable Xdebug profiling per-request with XDEBUG_PROFILE triggers; never leave it on globally.
  • Sort Cachegrind output by Self Cost to find real hotspots, not just deep call stacks.
  • Blackfire's timeline separates PHP time from I/O time — essential for diagnosing slow queries vs. slow code.
  • Add .blackfire.yaml assertions to CI to catch performance regressions before they reach production.
  • High exclusive time + low call count = expensive single operation; high call count + low exclusive time = N+1 pattern.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Can I use Blackfire on a production server without impacting users?
Blackfire's overhead is low enough for production use when triggered selectively via the browser extension or CLI. Avoid enabling continuous profiling on every request; use targeted triggers instead.
Q02 What is the difference between inclusive and exclusive time in a Blackfire call graph?
Inclusive time includes the time spent in a function plus all functions it calls. Exclusive time is only the time spent inside that function itself. High exclusive time pinpoints where work is actually happening.
Q03 Do I need both Xdebug and Blackfire, or is one enough?
They serve different purposes. Xdebug gives a complete call graph ideal for local exploration. Blackfire provides timeline views, CI assertions, and lower overhead for staging environments. Using both gives you the full picture.

Continue reading

More Articles

View all