reducing lazy services and autowiring overhead
Autowiring makes Symfony development convenient, but used carelessly it creates unnecessary objects on every single request. With lazy services, ghost objects and service subscribers, kernel boot performance can be noticeably improved without giving up autowiring entirely.
Table of contents
- 1. Why autowiring affects boot performance
- 2. Understanding eager injection versus lazy loading
- 3. Lazy services with the Autoconfigure attribute
- 4. Ghost objects in PHP 8.4 instead of ProxyManager
- 5. Service subscriber instead of full dependency injection
- 6. Lazy event listeners and tagged services
- 7. Identifying expensive services in your own project
- 8. Measuring boot overhead: Xdebug and Blackfire combined
- 9. Injection strategies in direct comparison
- 10. Summary
- 11. FAQ
1. Why autowiring affects boot performance
Symfony kernel boot performance depends directly on how many objects are actually instantiated before a controller even runs its logic. Autowiring is a double edged sword here: it saves developers from manually wiring dependencies, but implicitly leads to every injected class in a constructor actually being instantiated as soon as the parent service is needed, regardless of whether this specific dependency is even used in the current request.
The problem gets worse as the application grows. A controller with ten injected services, most of which are only needed in rare code paths, still fully instantiates all ten objects on every call, including their own transitive dependencies. For kernel boot performance, this means a significant share of object creation per request is pure waste, because the instantiated objects are never actually used in that specific request.
2. Understanding eager injection versus lazy loading
The default mechanism of Symfony's dependency injection is eager injection: as soon as a service is fetched from the container, the container immediately instantiates every dependency declared in the constructor, recursively down to the lowest level. This is sensible and unproblematic for most services, but becomes a problem once a dependency is itself expensive, for example because it already opens a network connection or parses a large configuration file inside its constructor.
Lazy loading deliberately reverses this behavior. Instead of the real class, the container injects a placeholder object that implements exactly the same interface, but only instantiates the actual class once a method is really called on that placeholder. For Symfony kernel boot performance, this means expensive but rarely used dependencies only cost time when they are truly needed, instead of on every single request regardless of actual demand.
3. Lazy services with the Autoconfigure attribute
The simplest way to mark a service as lazy is the #[Autoconfigure(lazy: true)] attribute directly on the service class. Symfony then automatically generates the necessary proxy or ghost object logic when compiling the container, without developers having to write an interface implementation themselves. This marking is especially valuable for services that are rarely called but appear as a dependency in many constructors, such as a central audit logger or a PDF export service.
Important to understand: lazy loading only works reliably if the service is typed via an interface, not the concrete class directly. Without an interface, Symfony would have to generate a subclass of the concrete class, which is technically impossible for final classes. This restriction is one of the main reasons many Symfony projects consistently rely on interfaces for services, even if currently only a single implementation exists.
<?php
// src/Service/PdfExportService.php
declare(strict_types=1);
namespace App\Service;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
/**
* Rarely called, but injected into many controllers as a dependency.
* Marking it lazy avoids constructing the heavy PDF engine on every boot.
*/
#[Autoconfigure(lazy: true)]
final class PdfExportService implements PdfExportInterface
{
public function __construct(
private readonly PdfEngineFactory $engineFactory, // expensive to build
) {
}
public function export(array $data): string
{
return $this->engineFactory->create()->render($data);
}
}
4. Ghost objects in PHP 8.4 instead of ProxyManager
Up to and including PHP 8.3, Symfony implements lazy services via subclasses generated at runtime by ProxyManager, which causes additional reflection overhead the first time each proxy is created. With PHP 8.4 and native support for lazy objects via ReflectionClass::newLazyGhost(), Symfony instead uses real engine level ghost objects, which further significantly reduces the overhead for kernel boot performance, because no additional class code has to be generated at runtime.
The practical difference for developers is minimal, since Symfony makes this choice transparently based on the PHP version. Important though: projects on older PHP versions can expect an additional performance gain from upgrading to PHP 8.4 when using lazy services heavily, with no changes to their own code at all, because engine ghost objects cause significantly less overhead per proxy creation than generated ProxyManager classes.
5. Service subscriber instead of full dependency injection
For controllers with many conditionally used dependencies, Symfony offers an even more targeted alternative to lazy services: the ServiceSubscriberInterface. Instead of declaring every dependency in the constructor, the class defines a getSubscribedServices() method that returns a list of required service IDs. The container then injects not real instances but a special ServiceLocator, which only instantiates services on explicit retrieval via $this->container->get('service.id').
This approach is even more explicit than plain lazy loading, because the developer controls precisely when which service is actually fetched. For kernel boot performance, this is especially relevant for controllers that bundle many possible actions with different dependencies each, such as an admin controller with ten different export formats, of which only one is ever actually needed per request.
<?php
// src/Controller/AdminExportController.php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Contracts\Service\Attribute\SubscribedService;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
/**
* Only one export format is used per request, but the controller
* supports many. ServiceSubscriberInterface avoids instantiating
* all of them on every single call.
*/
final class AdminExportController extends AbstractController implements ServiceSubscriberInterface
{
public function export(string $format): Response
{
// Only the requested exporter gets instantiated, not all of them
$exporter = $this->container->get($format . '.exporter');
return $exporter->export();
}
#[SubscribedService]
private function csvExporter(): CsvExporter
{
return $this->container->get(__METHOD__);
}
}
6. Lazy event listeners and tagged services
An often overlooked boot cost factor is event listeners. By default, Symfony instantiates every registered listener as soon as the corresponding event is dispatched, even if the listener is only relevant for very specific conditions. In applications with many bundles and correspondingly many listeners registered on frequent events such as kernel.request, this overhead adds up measurably per request.
Symfony's event dispatcher already supports lazy loading for listeners out of the box via container tags, so the listener class itself is only instantiated once the event actually fires and the listener stays registered. For kernel boot performance, it usually suffices to ensure listeners with expensive constructor dependencies are not unnecessarily registered broadly on very frequent events like kernel.request, but instead on more specific events that fire less often.
7. Identifying expensive services in your own project
To find out which services in a specific project would actually benefit from lazy loading, a combination of debug:container and targeted profiling helps. Services with many transitive dependencies that themselves open external resources such as HTTP clients, database connections or file system access inside their constructor are the best candidates for lazy services, because their construction is disproportionately expensive compared to simple value object services.
A practical starting point: services used only in a single, rarely called controller or command, but part of a central, frequently injected aggregate service, such as a NotificationDispatcher that has ten different channel implementations injected into its constructor even though usually only one channel is used per request. Exactly such aggregate services are the most rewarding starting point for targeted lazy loading.
8. Measuring boot overhead: Xdebug and Blackfire combined
To prove the actual effect of lazy services on kernel boot performance, intuition is not enough. A simple but effective approach: create a Blackfire profile before and after converting an expensive aggregate service to lazy loading, and directly compare how much the number of actually instantiated objects and the total runtime of the constructor call have changed.
In addition, bin/console debug:container --show-hidden gives the total count of all registered services as a rough indicator of the potential attack surface. The larger this number, the more likely a systematic review of the most frequently injected aggregate services for unused but still eagerly instantiated dependencies pays off.
# List every registered service as a rough indicator of container complexity
php bin/console debug:container --show-hidden | wc -l
# Inspect a specific aggregate service and its declared dependencies
php bin/console debug:container App\\Service\\NotificationDispatcher
9. Injection strategies in direct comparison
The choice between eager injection, lazy services and service subscriber depends on the specific use case. The following overview shows which strategy makes the most sense for kernel boot performance in which situation.
| Strategy | When the object is created | Implementation effort | Recommendation |
|---|---|---|---|
| Eager injection (default) | Immediately at constructor call | None | For fast, frequently used services |
| Lazy service (Autoconfigure) | Only on first method use | Low, one attribute | Expensive, rarely used individual services |
| Service subscriber | Only on explicit retrieval | Medium, custom locator logic | Controllers with many conditional paths |
| Lazy event listener | Only when the event actually fires | None, default container behavior | Listeners on rare events |
In practice these strategies complement rather than exclude each other. A typical pattern: the central aggregate service itself uses a service subscriber to load its ten channel implementations on demand, while each individual channel implementation is additionally marked as a lazy service in case it itself has expensive but rarely used dependencies. This combination minimizes actual object creation at every level of the dependency chain.
Mironsoft
Symfony boot performance, dependency injection audits and refactoring
Does your application instantiate too many objects on every request?
We identify expensive aggregate services in your Symfony application, deliberately introduce lazy services and service subscribers, and prove the improvement with concrete before-and-after profiles.
DI audit
Identify expensive, unnecessarily eagerly injected services in the container
Refactoring
Cleanly introduce lazy services and service subscribers into existing code
Validation
Before-and-after profiles with Blackfire for solid success measurement
10. Summary
Symfony kernel boot performance depends substantially on how many objects are actually instantiated before the real application logic runs. Autowiring and eager injection are the right choice for most services, but lead to unnecessary overhead on every request for expensive, rarely used dependencies. Lazy services via the Autoconfigure attribute, native ghost objects in PHP 8.4, and the targeted use of service subscribers solve this problem without giving up the benefits of autowiring entirely.
The pragmatic approach for existing projects: deliberately identify the most frequently injected aggregate services, analyze their transitive dependencies with profiling tools like Blackfire, and selectively introduce lazy loading wherever construction is genuinely expensive and rarely used. This targeted, measurable approach noticeably reduces kernel boot performance load without introducing unnecessary complexity into the rest of the code, which is already fast.
Symfony Kernel Boot Performance — the essentials at a glance
Lazy services
#[Autoconfigure(lazy: true)] delays instantiation until the first actual method use.
Interface requirement
Lazy loading needs an interface type, no final concrete classes without an interface.
Service subscriber
Explicit, controlled loading via a locator, ideal for controllers with many conditional paths.
Measurement
Before-and-after profiles with Blackfire prove the actual effect instead of relying on intuition.