Un appel LLM prend entre 500 ms et 30 secondes. Si ton API utilisateur attend ça en synchrone, tu vas voir des timeouts ou des PHP-FPM workers saturés. La solution standard : queue. Et en Symfony, c'est Messenger.
Le flux
Le message
// src/Message/CallLlm.php
namespace App\Message;
final class CallLlm
{
public function __construct(
public readonly string $conversationId,
public readonly string $prompt,
public readonly string $model = 'claude-sonnet-4-5',
) {}
}
Le handler
// src/MessageHandler/CallLlmHandler.php
namespace App\MessageHandler;
use App\Message\CallLlm;
use App\Service\AnthropicClient;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
final class CallLlmHandler
{
public function __construct(
private AnthropicClient $client,
private EntityManagerInterface $em,
) {}
public function __invoke(CallLlm $msg): void
{
$response = $this->client->send($msg->prompt, $msg->model);
// Persiste la réponse, déclenche un event front (Mercure, WebSocket)
$this->em->getConnection()->executeStatement(
'UPDATE conversation SET reply = ?, status = "ready" WHERE id = ?',
[$response, $msg->conversationId]
);
}
}
Le controller
public function ask(Request $request, MessageBusInterface $bus): JsonResponse
{
$conversationId = $this->createConversation($request->get('prompt'));
$bus->dispatch(new CallLlm($conversationId, $request->get('prompt')));
return new JsonResponse(['conversationId' => $conversationId, 'status' => 'pending']);
}
Config minimale
# config/packages/messenger.yaml
framework:
messenger:
transports:
async: 'doctrine://default?queue_name=llm'
routing:
App\Message\CallLlm: async
Lance le worker : php bin/console messenger:consume async -vv. C'est tout.
Patterns qui ont marché en prod
- Front polling ou Mercure. Polling toutes les 2 s sur l'endpoint
/conversations/{id}si tu débutes. Mercure ou WebSocket quand tu auras le besoin. - Retry exponentiel.
middleware: [send_after, retry_dlq]pour absorber les 429 et les timeouts API. - Worker dédié. Un service systemd qui maintient un worker tournant, pas un cron toutes les minutes.
- Concurrence limitée.
--limit=Net plusieurs workers par instance pour saturer ton budget de tokens, pas plus.