Blackfire & Xdebug Profiling in Laravel: Finding Real Bottlenecks
#laravel #performance #profiling #blackfire #xdebug

Blackfire & Xdebug Profiling in Laravel: Finding Real Bottlenecks

3 min read Mohamed Said Mohamed Said

Why Guessing Is Expensive

Most performance work in Laravel starts with a hunch: "It's probably the database" or "Maybe we need Redis here." Hunches are expensive. A profiler gives you a call graph with wall-clock time and memory allocations per function call. You stop optimising the wrong thing.

This article covers two tools with distinct roles:

  • Xdebug — free, local, great for deep call-stack traces and code-coverage-adjacent profiling.
  • Blackfire — commercial (free tier available), designed for continuous profiling, CI assertions, and production-safe sampling.

Xdebug: Local Call-Graph Profiling

Enable profiling output in php.ini (or a .ini drop-in for Sail/Herd):

[xdebug]
xdebug.mode = profile
xdebug.output_dir = /tmp/xdebug
xdebug.profiler_output_name = cachegrind.out.%p.%r

Trigger a single request profile without profiling every request:

xdebug.start_with_request = trigger

Then hit the route with the trigger query string:

curl "http://localhost/api/reports/monthly?XDEBUG_PROFILE=1"

Open the resulting cachegrind.out.* file in QCacheGrind (macOS: brew install qcachegrind). Sort by Self Cost to find functions that consume time independent of their callees — this is where actual work happens, not just delegation.

What to Look For

  • PDOStatement::execute called hundreds of times → N+1 query.
  • Illuminate\View\Compilers\BladeCompiler::compileString in production → missing compiled view cache (php artisan view:cache).
  • serialize / unserialize in a hot path → over-eager session or cache serialisation.

Blackfire: Profiling With Intent

Blackfire's agent runs as a sidecar. Install the CLI and browser extension, then profile from the terminal:

blackfire curl http://localhost/api/reports/monthly

The output is a URL to an interactive call graph. More useful for CI is the assertion system.

Writing Performance Assertions

Create .blackfire.yaml at the project root:

tests:
  "Monthly report endpoint is fast enough":
    path: /api/reports/monthly
    assertions:
      - "main.wall_time < 300ms"
      - "metrics.sql.queries.count < 10"

Run in CI:

blackfire validate --reference=1 --scenario=monthly_report

This turns performance into a first-class contract. A PR that introduces an N+1 fails the build before it ships.

Profiling Queued Jobs

Blackfire can profile CLI processes, which means you can profile a job in isolation:

blackfire run php artisan queue:work --once

Pair this with a seeded database and a known job payload so the profile is reproducible. Look for unexpected filesystem calls (file_get_contents on config files that should be cached) or repeated model hydration.


Practical Workflow

// Temporarily wrap a suspect code path with DB query logging
DB::enableQueryLog();

$report = app(MonthlyReportBuilder::class)->build($tenant);

dd(DB::getQueryLog()); // count, bindings, time per query

This is a quick sanity check before reaching for a profiler. If you see 80 queries for a report that should need 3, you already know where to look. The profiler then tells you which call site is responsible.


Takeaways

  • Use Xdebug locally for deep call-graph exploration; sort by Self Cost, not Inclusive Cost.
  • Use Blackfire for repeatable, assertion-driven profiling in CI and staging.
  • Profile queued jobs the same way you profile HTTP requests — they have the same bottleneck patterns.
  • DB::enableQueryLog() is a fast first filter before opening a full profiler.
  • Never optimise without a baseline measurement; profilers give you before/after proof.
  • Blade view compilation, config loading, and session serialisation are common non-obvious hotspots.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Can I use Blackfire in production without impacting all users?
Yes. Blackfire uses a request-triggered sampling model. Only requests carrying the Blackfire header (injected by the browser extension or CLI) are profiled. Normal traffic is unaffected, making it safe for production use.
Q02 Xdebug slows my app significantly — is that a problem for profiling?
Xdebug adds overhead, so absolute wall-clock numbers are inflated. However, the *relative* cost between functions remains proportional, so the call graph still correctly identifies which code paths are expensive. Use Blackfire when you need accurate wall-clock assertions.
Q03 How do I profile a specific Artisan command with Blackfire?
Run `blackfire run php artisan your:command` from the terminal. Blackfire wraps the process, collects the profile, and uploads it to your dashboard exactly as it does for HTTP requests.

Continue reading

More Articles

View all