Audit logging is the backbone of any enterprise application. Whether you are building a healthcare portal, a fintech app, or an internal CRM where tracking "who did what and when" is legally mandated, audit logs are non-negotiable.
However, adding an audit trail often introduces a massive performance penalty. By default, every time you flush an entity in Doctrine, the audit system must compute change-sets, serialize old and new data, and execute additional INSERT statements to your database. In a high-traffic Symfony API, synchronous auditing can easily double your response times.
we're going to fix that. We will learn how to dispatch audit logs asynchronously using Symfony Messenger and the AuditTrailBundle.
If you are looking for the best Symfony audit log solution that doesn't sacrifice performance, this is for you.
The Synchronous Bottleneck
Before we optimize, let's understand the problem. In traditional audit logging, the lifecycle looks like this:
- You create or update a
Productentity. - You call
$entityManager->flush(). - An
onFlushorpostFlushlistener intercepts the operation. - The listener builds an
AuditLogentity (computes diffs, extracts user context, grabs IP addresses). - The listener forces another synchronous
INSERTinto the database.
In our internal benchmarks using an in-memory SQLite database, a standard synchronous Doctrine flush with audit logging takes ~10.2ms. In a real-world scenario with a networked relational database (MySQL/PostgreSQL), network latency can easily push this to 30-50ms per request. And if you are doing bulk inserts? The latency stacks up quickly.
The Solution: Asynchronous Queue Transport
The AuditTrailBundle solves this elegantly using a Split-Phase Architecture and native support for Symfony Messenger.
Instead of waiting for the database to write the audit log, the bundle serializes the audit data into an AuditLogMessage and pushes it to a message queue (like RabbitMQ, Redis, or Doctrine). A background worker then processes the queue, validating the integrity of the log and inserting it into the database.
By offloading the I/O operations to a worker, your main user request finishes instantly.
Step-by-Step Guide: Implementing Async Symfony Audit Logs
Here is how you can set up asynchronous audit logging in your Symfony application in under 5 minutes.
Step 1: Install the Bundle
First, install the rcsofttech/audit-trail-bundle via Composer:
composer require rcsofttech/audit-trail-bundle
Step 2: Configure Symfony Messenger
Before enabling the async transport, ensure you have a transport configured in Symfony Messenger. Let's create an audit_trail transport in config/packages/messenger.yaml:
framework:
messenger:
transports:
# We use an async transport (e.g., AMQP, Redis, or Doctrine)
audit_trail: '%env(MESSENGER_TRANSPORT_DSN)%'
Step 3: Enable the Queue Transport
Now, tell the AuditTrailBundle to use the queue transport instead of the default synchronous Doctrine transport. Update your config/packages/audit_trail.yaml:
audit_trail:
transports:
# Disable the default synchronous transport
doctrine: false
# Enable the async queue transport
queue:
enabled: true
# Optional: Specify a custom bus if you use multiple message buses
bus: 'messenger.bus.default'
Step 4: Run the Consumer
In your production or local environment, start the Messenger worker to consume the audit messages in the background:
php bin/console messenger:consume audit_trail -vv
That's it! Your Symfony application is now dispatching audit logs asynchronously.
Under the Hood: How it Works
Let's look at the technical architecture of how AuditTrailBundle achieves this.
When you flush your EntityManager, the EntityProcessor captures the raw changes and delegates the dispatch to the AuditDispatcher. If the Queue transport is enabled, the QueueAuditTransport takes over.
1. Message Creation
The bundle prevents heavy ORM entity serialization by transforming the raw AuditLog into a lightweight, decoupled AuditLogMessage DTO:
$message = AuditLogMessage::createFromAuditLog($log, $entityId);
2. Extensibility via Events
Before the message is dispatched to the bus, the bundle fires an AuditMessageStampEvent. This allows you to programmatically attach Messenger stamps (like a DelayStamp or custom routing rules) to the audit log without overriding core services.
// Example: Adding a 5-second delay to all audit logs
public function onAuditMessageStamp(AuditMessageStampEvent $event): void
{
$event->addStamp(new DelayStamp(5000));
}
3. Cryptographic Signatures (HMAC)
If you have data integrity enabled (audit_trail.integrity.enabled: true), the bundle signs the JSON payload of the message and attaches a SignatureStamp. When the worker consumes the message, it verifies the signature to ensure the audit log wasn't tampered with while sitting in the queue (e.g., Redis or RabbitMQ).
Hard Data: The Benchmarks
Does async logging actually make a difference? We ran rigorous automated benchmarks using PHPBench to measure the throughput of the QueueAuditTransport against the synchronous Doctrine approach.
Here are the results:
-
Worker Throughput: The background Messenger worker that un-serializes the
AuditLogMessage, verifies its HMAC signature, and flushes it to the database takes roughly ~1.45ms per message. That equals a staggering ~690 messages processed per second per single worker thread. -
Main Request Impact: In pure CPU time loops, serializing a message into the queue takes about
~12.2mssynchronously. While this sounds comparable to a pure in-memory SQLite flush (~10.2ms), the magic happens in a real networked environment. In production, a MySQLINSERTover the network introduces significant IO wait times. By pushing to Redis or RabbitMQ instead, your main user request bypasses database IO completely. - Bulk Processing: At scale, the overhead of the bundle drops to just ~3ms per entity, making it perfect for highly intensive import scripts.
Conclusion
Audit logging shouldn't be a bottleneck for your application's growth. By switching to an asynchronous architecture using Symfony Messenger and the AuditTrailBundle, you decouple your core business logic from compliance requirements, leading to virtually zero perceived overhead for your users.
Ready to supercharge your Symfony audit logs? Check out the AuditTrailBundle on GitHub, give it a star, and drop any questions in the discussions!
Top comments (0)