This content originally appeared on HackerNoon and was authored by MattLeads
In our previous article, we developed a basic AI agent that performed well enough when run from the console. However, this simple, linear approach has significant limitations in terms of speed and resource efficiency.
Network connections to remote services and data retrieval from external servers are inherently time-consuming operations. This is due to the overhead of establishing a connection, transmitting data over the Internet, and closing the session — all before considering potential server load from other requests. What’s more, a critical component — such as the mailbox servers, the LLM model servers, or the outbound email servers — might be temporarily unavailable.
By adopting an asynchronous approach for interacting with each external resource, we can dramatically accelerate the overall system. This not only boosts performance but also enhances system resilience, minimizing downtime when external resources are temporarily unavailable. This article will show you how to leverage asynchronous programming to create a faster, more robust AI agent.
Building a Resilient AI Agent: Handling Service Disruptions Asynchronously
Imagine a scenario where the LLM model server becomes temporarily unavailable. In a robust, asynchronous system, this isn’t a showstopper. Instead of halting all operations, we can simply continue to retrieve emails, queue them up for analysis, and then, once the LLM server is back online, independently send the queued data for processing without disrupting other business logic.
For the sake of this simple example, we won’t delve into data persistence or a comprehensive error-handling strategy for network issues. However, I will demonstrate how you can easily decouple your application components by moving core modules to an asynchronous operating mode.
A perfect tool for this is the Symfony Messenger Component. It is an outstanding solution for building asynchronous workflows and facilitates seamless communication between different parts of a single application, as well as between entirely separate applications.
To install the Symfony Messenger Component using Composer, you just need to run a single command in your project’s root directory.
\
composer require symfony/messenger
Flexible Transports: Powering Symfony Messenger
The Symfony Messenger Component is incredibly versatile, supporting a wide range of transports such as AMQP, Doctrine, Redis, Amazon SQS, and many more. This flexibility allows you to select the best transport for each specific task within your application.
Each transport can be configured for both synchronous and asynchronous operations. You can also assign different priorities, set message limits, and configure other useful parameters that are essential for managing high-load production environments. This fine-grained control greatly simplifies the deployment of mission-critical applications.
A key advantage of the Messenger component is its transport-agnostic nature. This means you can easily switch between transports as your needs evolve. For example, you could start with Redis, transition to AMQP later, and then, for a super-high-traffic production environment, seamlessly switch to a cloud-based solution like Amazon SQS. This freedom of choice ensures your architecture can scale and adapt over time.
For our project, we will be using Redis. This transport is a highly efficient choice for message queuing, leveraging Redis Streams to handle messages reliably. This transport requires the Redis PHP extension (php-redis, version 4.3 or higher) and a running Redis server (5.0 or higher).
To install the necessary components, simply run the following command in your terminal:
\
composer require symfony/redis-messenger
Configuring the Redis Transport DSN
Define environment variable named MESSENGERTRANSPORTDSN
\
MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
Let’s break down this DSN:
redis://: The protocol specifying we’re connecting to a Redis service.
localhost: The hostname of your Redis server. Replace this with the server’s IP address or hostname if it’s not running locally.
6379: The default port for Redis. Change this if your server uses a different port.
/messages: This is the name of the Redis stream (or queue) where messages will be stored. This allows you to have multiple, separate queues on the same Redis server.
By defining this variable, your Symfony Messenger configuration can dynamically pull the connection details without hardcoding them, making your application more portable and secure.
Boost Performance and Decouple Components with a CQRS-Based Bus Architecture
Data buses are built on top of the transport layer. They not only structure the flow of messages but also allow for additional pre- and post-processing via middleware as a message enters the bus.
For our project, I propose creating and maintaining three primary data buses right from the start. This approach is based on the CQRS (Command Query Responsibility Segregation) pattern, which we might explore in more detail in future articles.
\
- Command Bus
A command is a message that tells the application to perform a specific action that changes its state. It represents a single, imperative instruction.
Main purpose is to change data or trigger a business process. It’s an instruction to “do this” (e.g., CreateOrder, ChangeProductName).
A command should have only one handler that executes the logic and doesn’t return a value. Its success or failure is handled through exceptions.
\
- Query Bus
A query is a message that asks for data without changing the application’s state.
Main purpose is to retrieve data from the application. It’s a request to “get me this” (e.g., GetProductDetails, ListAllUsers).
A query should have only one handler that fetches the required data and query handler always returns the requested data.
\
- Event Bus
An event is a message that announces that something noteworthy has already happened in the application. It is a record of a past action.
Main purpose is to notify other parts of the system about a change so they can react to it. It’s a statement that “this happened” (e.g., UserCreated, OrderShipped).
An event can have zero, one, or many listeners or subscribers. Events are often handled asynchronously to decouple different parts of the system, which is a key benefit of this approach.
Messenger Component Configuration: A Step-by-Step Guide
Here is the complete config/packages/messenger.yaml configuration to set up your message buses, transports, and routing, including support for the Symfony Notifier component.
\
framework:
messenger:
default_bus: core.command.bus
buses:
core.command.bus:
default_middleware:
enabled: true
allow_no_handlers: false
allow_no_senders: false
core.query.bus:
default_middleware:
enabled: true
allow_no_handlers: true
allow_no_senders: true
core.event.bus:
default_middleware:
enabled: true
allow_no_handlers: true
allow_no_senders: true
transports:
main.transport:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
'*': [main.transport]
This setup enables your AI agent to handle tasks asynchronously, which is crucial for building a responsive and resilient application.
Building an Asynchronous Workflow: Summarizing Emails with an AI Agent
Now, let’s create the core asynchronous process that will interact with our LLM. This crucial step allows our application to offload a time-consuming task — communicating with an external API — without blocking the main application thread. This makes our AI agent more efficient and resilient.
The workflow will be as follows: a message is dispatched to the bus, which will asynchronously connect to the LLM model. It will send the retrieved emails for summarization and, upon a successful response, will initiate a new asynchronous process to send a notification via our chosen channel.
To achieve this, we’ll need two key components: the AIAgentSummarizeMessage and its corresponding handler, AIAgentSummarizeMessageHandler.
The AIAgentSummarizeMessage Class
This class acts as a data transfer object (DTO). It encapsulates all the necessary information for our LLM summarization task, ensuring the message is self-contained and easy to handle.
\
namespace App\Message\Command;
use App\DTO\DataCollection;
readonly class AIAgentSummarizeMessage {
public function __construct(
private DataCollection $dataCollection,
private string $prompt,
)
{
}
public function getDataCollection(): DataCollection
{
return $this->dataCollection;
}
public function getPrompt(): string
{
return $this->prompt;
}
}
$dataCollection: This property holds the collection of email objects or other data that our AI agent needs to analyze.
$prompt: This string will contain the specific instructions for the LLM, guiding it on how to summarize the provided data.
The AIAgentSummarizeMessageHandler Class
The handler is a dedicated class that listens for and processes AIAgentSummarizeMessage objects. In Symfony Messenger, the core logic is placed inside a single public method named __invoke. This method is automatically called whenever a message of the correct type appears in the bus.
\
namespace App\Handler\Command;
use App\DTO\MailMessage;
use App\Message\Command\AIAgentSummarizeMessage;
use App\Message\Command\NotifySummarizedMessage;
use App\Service\AIAgentService;
use App\Service\AIProvider\GeminiAIAgentService;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Notifier\Notification\Notification;
#[AsMessageHandler(bus: 'core.command.bus', fromTransport: 'main.transport')]
readonly class AIAgentSummarizeMessageHandler
{
public function __construct(
private AIAgentService $aiAgentService,
private GeminiAIAgentService $geminiAIAgentService,
protected MessageBusInterface $messageBus
){
}
public function __invoke(AIAgentSummarizeMessage $message): void
{
$result = $this->aiAgentService->action($this->geminiAIAgentService, $message->getDataCollection(), $message->getPrompt());
if (!is_null($result)) {
$this->messageBus->dispatch(
new Envelope(
new NotifySummarizedMessage(
new MailMessage(
'Summary message',
null,
'summary@mailbox.com',
$result,
null
),
Notification::IMPORTANCE_MEDIUM
)
)
);
}
}
}
Finalizing the Workflow: Creating the Asynchronous Notification Process
To complete our resilient workflow, we’ll create a second message and handler pair. This ensures that sending a notification is a completely separate and asynchronous process from the LLM summarization. This decoupling is a cornerstone of a robust system; if your email server is temporarily unavailable, the summarization process will still complete successfully, and the notification can be retried later without affecting other services.
For this step, we’ll need a new message, NotifySummarizedMessage, and its dedicated handler, NotifySummarizedMessageHandler.
The NotifySummarizedMessage Class
This message will carry all the information needed to send a notification.
\
namespace App\Message\Command;
use App\DTO\MailMessage;
use Symfony\Component\Notifier\Notification\Notification;
readonly class NotifySummarizedMessage {
public function __construct(
private MailMessage $notification,
private string $importance = Notification::IMPORTANCE_MEDIUM
)
{
}
public function getNotification(): MailMessage
{
return $this->notification;
}
public function getImportance(): string
{
return $this->importance;
}
}
$notification: This property holds the MailMessage object. It contains the email’s subject, body, and other relevant details.
$importance: This string allows you to specify a priority (e.g., ‘high’, ‘urgent’) to control which Notifier channel is used for delivery. This is highly useful for sending different types of notifications via different services (e.g., urgent alerts via SMS or Messengers, regular updates via email).
The NotifySummarizedMessageHandler Class
This handler will listen for NotifySummarizedMessage objects on the bus. Its sole purpose is to use the Symfony Notifier component to send the message to the specified channel, completing our asynchronous chain.
Just like our previous handler, it will contain a single __invoke method that is automatically triggered by the Messenger component.
\
namespace App\Handler\Command;
use App\Message\Command\NotifySummarizedMessage;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Notifier\Notification\Notification;
use Symfony\Component\Notifier\NotifierInterface;
use Symfony\Component\Notifier\Recipient\Recipient;
#[AsMessageHandler(bus: 'core.command.bus', fromTransport: 'main.transport')]
readonly class NotifySummarizedMessageHandler
{
public function __construct(
private NotifierInterface $notifier
)
{
}
public function __invoke(NotifySummarizedMessage $message){
$notification = (new Notification(
$message->getNotification()->getSubject()))
->content($message->getNotification()->getBody())
->importance($message->getImportance());
$recipient = new Recipient(
$message->getNotification()->getTo()
);
$this->notifier->send($notification, $recipient);
}
}
\ By completing this step, our AI agent now has a fully decoupled, resilient, and scalable workflow. The LLM summarization and the notification processes are completely independent, ensuring that your system can handle failures gracefully and continue to perform under load.
Configuring Message Routing for Your AI Agent
While our current setup uses a single asynchronous transport, it’s a best practice to explicitly define routing rules for each message type. This approach makes your application’s message flow clear, maintainable, and easily scalable for future growth.
By configuring these rules in config/packages/messenger.yaml, we’re explicitly telling Symfony where to send each message. This prepares our system for a more complex architecture with different transports for various tasks, priorities, or external services.
\
framework:
messenger:
default_bus: core.command.bus
...
routing:
'*': [main.transport]
App\Message\Command\AIAgentSummarizeMessage: [main.transport]
App\Message\Command\NotifySummarizedMessage: [main.transport]
By adding these two lines, we’ve clearly defined the flow for our entire asynchronous workflow.
Updating the Summarize Command: Dispatching Messages to the Bus
Now that our asynchronous workflow is in place, we need to update our SummarizeCommand. Its new purpose is no longer to perform the time-consuming tasks itself, but to act as a lightweight dispatcher.
\
declare(strict_types=1);
namespace App\Command;
use App\Message\Command\AIAgentSummarizeMessage;
use App\Service\ImapMailService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
#[AsCommand(name: 'app:summarize', description: 'Summarize Command')]
class SummarizeCommand extends Command
{
public function __construct(
private readonly ImapMailService $imapMailService,
private readonly MessageBusInterface $messageBus,
){
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$emailCollection = $this->imapMailService->fetchEmails(2);
$this->messageBus->dispatch(
new Envelope(
new AIAgentSummarizeMessage(
$emailCollection,
'I have emails. Please summarize them into a concise overview (100-150 words) focusing on key decisions, action items, and deadlines. Use bullet points to organize the summary by email or theme, whichever is clearer. Here’s the email content:'
)
)
);
return Command::SUCCESS;
}
}
This change is fundamental to our non-blocking approach. The command will now finish its job instantly, freeing up the terminal while the long-running LLM and notification processes run silently in the background.
Putting It All Together: Launching Your Asynchronous AI Agent
We’ve successfully built a complete asynchronous workflow for your AI agent. Every component — from retrieving emails to summarizing them with an LLM and sending a notification — is now decoupled and runs in the background.
Starting the Messenger Consumer 🚀
First, open a new terminal window and run the following command. This command starts the consumer, a dedicated process that will continuously listen to the Redis stream for new messages and process them in the background.
\
./bin/console messenger:consume --all -vv
This command will start the consumer, and you should see a message indicating it’s now listening for new messages (-vv). Leave this terminal window running, as it will handle all the background tasks.
Initiating the Mailbox Process 📬
With the consumer running, open a second terminal and execute command to initiate the main business logic. This is the command that triggers the entire asynchronous workflow by dispatching the first message (e.g., AIAgentSummarizeMessage) to the message bus.
\
./bin/console app:summarize
Watch the Magic Happen ✨
Once you run this, you will see a message confirming the dispatch. Simultaneously, in the first terminal, the consumer will immediately pick up the message from the queue and begin processing it, starting the LLM summarization and subsequent notification steps. This is the asynchronous approach in action.
This is the entire asynchronous chain working seamlessly:
- Your command dispatches a message and exits.
- The consumer listens to the queue, retrieves the message, and processes the LLM request in the background.
- Upon success, it dispatches a new notification message.
- The consumer then picks up and processes the new message to send the notification.
Conclusion
We have successfully built a resilient, high-performance AI agent that can handle complex, time-consuming tasks without ever blocking the main application.
In our upcoming articles, we will take this project to the next level. I’ll show you how to automate the entire process by:
Scheduling Mailbox Polling: We’ll set up a system to automatically check mailboxes at regular intervals.
Containerizing with Docker: We’ll package our entire application into a Docker container to ensure a consistent and portable environment.
Deploying with a Process Manager: We’ll explore two different methods for running our consumers in a production environment:
- Using a dedicated process manager like Supervisor to manage our workers.
- Running the consumers as a standalone, multi-process worker for optimal performance.
Each of these approaches has its own set of advantages and trade-offs, which we’ll thoroughly examine to help you choose the best deployment strategy for your needs.
Stay tuned — and let’s keep the conversation going.
This content originally appeared on HackerNoon and was authored by MattLeads

MattLeads | Sciencx (2025-09-03T04:22:48+00:00) Your AI Agent Is Too Slow—Here’s How to Fix It. Retrieved from https://www.scien.cx/2025/09/03/your-ai-agent-is-too-slow-heres-how-to-fix-it/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.