Symfony Profiler: Find Performance Bottlenecks in Seconds
AI generated
SF
{ }
Symfony · Profiler · Performance · Debugging · N+1
Symfony Profiler:
Find Performance Bottlenecks in Seconds

The Symfony Profiler is more than the debug bar at the bottom of the page. It shows precisely which database queries slow down a page, where event listeners consume excessive time, which caches are not hitting, and how much memory a request uses, all at the push of a button, without a single code change.

18 min read Profiler · Web Debug Toolbar · N+1 · Cache · Custom Collector Symfony 7.x · Doctrine ORM · PHP 8.4

1. What the Symfony Profiler Can Really Do

The Symfony Profiler is a built-in diagnostic tool that creates a complete runtime recording for every HTTP request. These profiles are stored as files in the var/cache/dev/profiler directory and are accessible via the /_profiler URL. Every profile contains the execution time of all Symfony kernel phases, all database queries with duration and execution plan, all Twig templates with rendering time, all cache operations, event listener execution times, and memory peaks. This level of detail is what external APM tools like Datadog or New Relic only deliver after elaborate instrumentation, the Symfony Profiler comes built in from the start.

The decisive advantage of the Symfony Profiler during development is that it provides context. Analyzing a slow page with the Profiler does not just show "200 ms for database queries", it shows exactly which SQL queries were executed, which parameters were bound, and how many identical queries ran multiple times. Without the Symfony Profiler, a developer would have to gather this information through manual logging or external tools. With the Profiler, finding the cause of a performance regression takes seconds. Profiler data is persisted and can still be analyzed after the request completes, which is invaluable for errors that only occur under specific conditions.

2. Web Debug Toolbar: the Most Important Panels

The Web Debug Toolbar, displayed at the bottom of the page in development mode, is the first diagnostic tool for performance problems in Symfony. The toolbar shows at a glance: HTTP status, routing match, controller class, number of database queries, Twig rendering time, cache hits and misses, memory peak and total execution time. A page showing an unusually high number of database queries is immediately identifiable as an N+1 problem, even before opening the full Symfony Profiler.

Clicking a number in the toolbar opens the full Symfony Profiler for the relevant request. The Doctrine panel shows every SQL query with execution time, parameters, and the stack trace that triggered the query. The Twig panel shows all rendered templates in a hierarchical structure with rendering times. The Security panel shows the authenticated user and all voter decisions. The Cache panel lists all cache operations. For AJAX requests that do not return a full HTML page, no toolbar icon is visible, but the profiles are still saved and remain accessible via /_profiler/latest.


<?php
// config/packages/web_profiler.yaml: Profiler configuration
// (default dev config, shown here for reference)

// web_profiler:
//   toolbar: true
//   intercept_redirects: false

// framework:
//   profiler:
//     only_exceptions: false  # Profile all requests, not just failed ones
//     collect: true
//     dsn: 'file:%kernel.cache_dir%/profiler'  # Where profiles are stored
//     collect_serializer_data: true  # Enable serializer panel

// To access profiles programmatically in tests:
// use Symfony\Bundle\FrameworkBundle\KernelBrowser;
// $client->enableProfiler();
// $client->request('GET', '/products');
// $profile = $client->getProfile();
// $queryCount = $profile->getCollector('db')->getQueryCount();

// Useful Profiler URLs:
// /_profiler             → list of recent profiles
// /_profiler/latest      → most recent profile
// /_profiler/{token}     → specific profile by token
// /_profiler/{token}/db  → directly to database panel

// To profile CLI commands (Symfony Console):
// Profiler data is written to var/cache/dev/profiler/
// Set SYMFONY_PROFILER=1 env var to enable profiling for console commands

3. Finding N+1 Queries with the Doctrine Panel

The N+1 query problem is the most common performance cause in Symfony applications using Doctrine ORM. The symptom: a product list with 50 products generates 51 SQL queries, one for the product list, and one for the category associated with each product. The Symfony Profiler Doctrine panel makes this immediately visible: 51 queries, of which 50 are nearly identical and differ only in the ID parameter. The stack trace of each query shows which code path triggered it.

The solution for N+1 queries, using the Symfony Profiler as the starting point, is eager loading via Doctrine's DQL or QueryBuilder with addSelect and leftJoin. Instead of 51 queries, a single query with a LEFT JOIN is executed, and the Symfony Profiler then shows a single query with a reasonable execution time. For more complex scenarios, Doctrine's EXTRA_LAZY loading for collections is recommended, which only loads on actual access, along with the Doctrine second-level cache integration, which further reduces database access for frequently retrieved entities. The Profiler remains the central tool for measuring the effect of these optimizations.

4. Analyzing Event Listener Performance

The Symfony Profiler shows all dispatched events and the time each listener took to execute in the Events panel. This is especially valuable in Symfony applications that use many event listeners, such as API Platform projects, security implementations with multiple listeners, or custom business logic driven by events. A listener that runs a database query on every request, for example to load user permissions, shows up in the Events panel with its total duration and can be identified as a performance problem.

A common finding in the Symfony Profiler: the security.firewall listener is slow because it loads a user from the database on every request, even though the user should be stored in the session. Or the kernel.request listener executes an unnecessary database query before the controller even runs. The Symfony Profiler shows the execution order of all listeners and their relative share of the total request time. Events with a high total duration are candidates for optimization: caching listener results, lazy-loading dependencies, or moving the work into asynchronous Symfony Messenger messages.

5. Cache Misses and Cache Hits in the Profiler

The Cache panel in the Symfony Profiler shows all cache operations: reads (get), writes (set), deletes, and whether the access was a hit or a miss. A high proportion of cache misses for a particular cache key indicates either that the cache TTL is too short, the cache is being cleared too often, or the cache key generation is too granular. The Symfony Profiler shows not just whether a cache miss occurred, but the exact key as well, which enables analysis of whether keys are formed consistently or whether different keys are unintentionally generated for identical data.

In Symfony applications using HTTP cache, the Performance panel in the Symfony Profiler provides information on cache-control headers and ESI fragments. If a page is theoretically cacheable but cache-control headers are preventing caching, the Profiler shows why, for example because an event listener called Response::setPrivate(). The Twig panel additionally shows which templates were compiled and which were loaded from the opcode cache. For Symfony projects with many Twig templates, the compilation overhead on the first call after a cache clear is directly measurable in the Profiler.


<?php

declare(strict_types=1);

namespace App\Repository;

use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/**
 * Product repository with N+1-prevention via eager loading.
 */
class ProductRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Product::class);
    }

    /**
     * Load products with category in a single query, prevents N+1.
     * Before: 1 query for products + N queries for categories = N+1 problem
     * After:  1 query with LEFT JOIN = Symfony Profiler shows 1 query
     *
     * @return Product[]
     */
    public function findAllWithCategory(): array
    {
        return $this->createQueryBuilder('p')
            // Eager-load the category relation, fetched in the same SQL query
            ->addSelect('c')
            ->leftJoin('p.category', 'c')
            ->orderBy('p.name', 'ASC')
            ->getQuery()
            ->getResult();
    }

    /**
     * Use DQL with INDEX BY to build a lookup map, avoids repeated array searches.
     * Result: associative array keyed by product ID.
     *
     * @return array<int, Product>
     */
    public function findAllIndexedById(): array
    {
        return $this->createQueryBuilder('p', 'p.id')
            ->getQuery()
            ->getResult();
    }
}

6. Timeline: Locating Bottlenecks Visually

The Timeline panel in the Symfony Profiler is the most powerful tool for analyzing complex performance problems. It shows the entire request lifecycle as a horizontal timeline: kernel bootstrap, routing, controller execution, template rendering, response finalization. Every phase is shown as a colored bar that indicates both the absolute duration and the relative share of the total time. If the controller bar takes up 80% of the total time, you know immediately that the performance work has to start in the controller or in the services it calls.

The timeline also shows nested span data: if one service calls another service that in turn triggers a database query, the hierarchy is visible in the Symfony Profiler timeline. This makes it possible, in a request with a total duration of 500 ms, to identify exactly the 200 ms span that represents an external HTTP request blocking on a response. For custom instrumentation, where you want your own services' execution time to appear in the timeline, you use the Symfony Stopwatch service, which integrates directly with the Symfony Profiler and adds spans to the timeline.

7. Writing a Custom Data Collector

The Symfony Profiler is extensible: custom data collectors add your own panels to the toolbar and to the profiler. This is useful for application-specific metrics, such as the number of processed business events, external API calls with latencies, feature flag states, or custom cache statistics. A custom collector implements the DataCollectorInterface and defines what data is collected, how it is displayed, and whether it appears in the toolbar.

For teams running their own services in Symfony projects, a custom collector is a valuable debugging tool. If an API client service makes five external HTTP calls, a custom collector can record every call with URL, duration, HTTP status, and response size. In the Symfony Profiler, you can then see at a glance which external service is slowing down the current request. The alternative, logging with manual log analysis, is considerably more time-consuming. The custom collector stores its data in a serializable form for the persisted profiles and makes it available for later analysis as well.


<?php

declare(strict_types=1);

namespace App\DataCollector;

use Symfony\Bundle\FrameworkBundle\DataCollector\AbstractDataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * Custom Profiler panel for tracking external API calls.
 * Appears as a dedicated panel in the Symfony Profiler.
 */
final class ExternalApiCollector extends AbstractDataCollector
{
    /** @var array<array{url: string, duration: float, status: int}> */
    private array $calls = [];

    /**
     * Called by the external API client to record each request.
     */
    public function recordCall(string $url, float $durationMs, int $httpStatus): void
    {
        $this->calls[] = [
            'url'      => $url,
            'duration' => $durationMs,
            'status'   => $httpStatus,
        ];
    }

    /**
     * Collect and serialise data when the request finishes.
     */
    public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
    {
        // Data must be serialisable, store in $this->data for Profiler persistence
        $this->data = [
            'calls'         => $this->calls,
            'total_calls'   => count($this->calls),
            'total_duration' => array_sum(array_column($this->calls, 'duration')),
        ];
    }

    /** @return array<array{url: string, duration: float, status: int}> */
    public function getCalls(): array
    {
        return $this->data['calls'] ?? [];
    }

    public function getTotalCalls(): int
    {
        return $this->data['total_calls'] ?? 0;
    }

    public function getTotalDuration(): float
    {
        return $this->data['total_duration'] ?? 0.0;
    }

    // Name used as panel ID and URL segment in the Profiler
    public static function getTemplate(): ?string
    {
        return '@App/data_collector/external_api.html.twig';
    }
}

8. Using the Profiler in PHPUnit Tests

The Symfony Profiler can be used in PHPUnit tests to automatically detect performance regressions. If a feature branch suddenly produces 50 instead of 5 database queries for a particular page, that should be caught in CI, not first during code review or in production. The KernelBrowser in Symfony tests has an enableProfiler() method that provides access to the profile after the request. With $client->getProfile()->getCollector('db')->getQueryCount(), you test that the number of SQL queries stays below a given threshold.

Performance tests using the Symfony Profiler are no substitute for load tests, but they catch the most common regressions at the unit test level. A test that ensures a product list with 100 products executes at most 3 SQL queries effectively prevents an N+1 problem from being introduced by a careless commit. The Symfony Profiler makes this kind of assertion possible without needing external performance monitoring tools. Combined with the custom collector from the previous section, application-specific metrics can be tested as well.

9. Symfony Profiler vs. External APM Tools

The Symfony Profiler is the right tool for the development phase. For production, you need a complementary monitoring system. The question is where the line between profiler and APM tool lies.

Criterion Symfony Profiler Datadog / New Relic Blackfire.io
Use case Development, testing Production Dev + production
Overhead No production overhead Measurable (1-5%) Low in probe mode
Symfony integration Native, zero config Agent + PHP extension Symfony bundle available
SQL analysis Complete with stack trace Aggregated over time Profile-based
Cost Free (open source) Paid Paid from production onward

The optimal strategy for Symfony projects combines both approaches: the Symfony Profiler for development work and for writing performance tests in CI, and an APM tool for production monitoring and alerting on performance degradation under load. Blackfire.io is the most natural complement to the Symfony Profiler, since it is built by the same developers and integrates seamlessly with the Symfony ecosystem. For smaller projects, the Symfony Profiler combined with structured logging and Grafana is often sufficient, APM tools are an investment that only pays off from a certain traffic level onward.

Mironsoft

Symfony Performance Analysis, Profiler Evaluation and Optimization

Solve Symfony Performance Problems Systematically?

We analyze Symfony applications using the Profiler, identify N+1 queries, cache misses and event listener bottlenecks, and implement the optimizations for measurable performance gains in your project.

Profiler Analysis

Systematic evaluation of Symfony Profiler data and identification of performance bottlenecks

Query Optimization

Fixing N+1 queries with Doctrine eager loading and DQL optimizations

Performance Tests

Writing PHPUnit tests with Profiler assertions that prevent performance regressions in CI

10. Summary

The Symfony Profiler is the most effective diagnostic tool for performance work in Symfony applications. The Doctrine panel uncovers N+1 queries with stack traces and shows exactly which code path triggers how many SQL queries. The Events panel makes slow listeners visible. The Cache panel shows hit/miss ratios for all cache pools. The timeline visualizes the entire request lifecycle and makes bottlenecks visible at a glance. Custom data collectors extend the Profiler with application-specific metrics.

The greatest return on investment lies in not using the Symfony Profiler only reactively, when a page is slow, but proactively as part of the development process. PHPUnit tests that check query counts via the Profiler API prevent performance regressions in CI before they reach production. Combined with the Stopwatch service for custom timing and an APM tool for production monitoring, a complete performance observability strategy emerges, one that is achievable in Symfony projects without external tools.

Symfony Profiler: the Essentials at a Glance

Finding N+1 Queries

The Doctrine panel shows all SQL queries with duration and stack trace. Multiple identical queries with different IDs = N+1 problem. Fix: eager loading via leftJoin + addSelect.

Timeline Analysis

The timeline shows the request lifecycle as a time bar. Large controller bar = work is in the service. Use the Stopwatch service for custom spans in the timeline.

Custom Collector

Implement DataCollectorInterface for your own panels. Make application-specific metrics like external API calls visible in the profiler.

Performance Tests

$client->enableProfiler() in PHPUnit. Query count assertions prevent N+1 regressions in CI. getCollector('db')->getQueryCount().

11. FAQ: Symfony Profiler and Performance

1What is the Symfony Profiler?
Built-in diagnostic tool: SQL queries, event times, cache ops, memory peak. Accessible via /_profiler. Profiles stored persistently.
2Enabling the Symfony Profiler?
Active by default in dev mode via toolbar: true. For tests: $client->enableProfiler(). Never in prod mode without access control.
3Finding N+1 queries?
Doctrine panel: many identical queries with different IDs = N+1. Fix: leftJoin + addSelect in the QueryBuilder for eager loading.
4Using the Profiler in PHPUnit?
$client->enableProfiler(). After the request: getProfile()->getCollector('db')->getQueryCount(). Detect N+1 regressions in CI automatically.
5Writing a custom data collector?
Implement DataCollectorInterface. Store serializable data in collect() into $this->data. A Twig template for the panel layout. Register the service with the data_collector tag.
6Using the timeline panel?
Shows the request lifecycle as a time bar. Wide bars = a lot of time. Use the Stopwatch service for custom timing, it appears automatically in the timeline.
7Performance overhead of the Profiler?
Yes in dev mode, disabled by default in prod mode. only_exceptions mode for staging: only failed requests are profiled.
8Profiler for API requests?
Yes. API requests are profiled the same way. Profile under /_profiler/latest or via the X-Debug-Token response header. Ideal for API Platform endpoints.
9Finding cache misses?
The Cache panel lists all operations with hit/miss status. High miss rate = TTL too short, clearing too often, or inconsistent key generation.
10When to use an APM tool as well?
For production: Datadog, New Relic or Blackfire.io for aggregated data over time, alerting on degradation, and multi-service tracing.