{"id":329,"date":"2026-05-20T21:20:57","date_gmt":"2026-05-21T01:20:57","guid":{"rendered":"https:\/\/juredev.com\/blog\/?p=329"},"modified":"2026-05-20T21:21:35","modified_gmt":"2026-05-21T01:21:35","slug":"procesamiento-asincrono-en-php-con-symfony-messenger","status":"publish","type":"post","link":"https:\/\/juredev.com\/blog\/2026\/05\/procesamiento-asincrono-en-php-con-symfony-messenger\/","title":{"rendered":"Procesamiento As\u00edncrono en PHP con Symfony Messenger"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Cuando una aplicaci\u00f3n PHP empieza a crecer, uno de los primeros problemas de rendimiento suele aparecer en el peor lugar posible: la experiencia del usuario.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Un registro que tarda demasiado.<br>Una compra que demora varios segundos.<br>Una API que parece \u00abcongelarse\u00bb antes de responder.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En muchos casos, el problema no es PHP en s\u00ed. El problema es ejecutar demasiadas tareas dentro de una misma petici\u00f3n HTTP. Y eso tiene soluci\u00f3n.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">El problema: tareas bloqueantes en la request<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Imaginemos un flujo t\u00edpico de registro de usuario:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>El usuario env\u00eda el formulario<\/li>\n\n\n\n<li>La aplicaci\u00f3n crea el usuario en base de datos<\/li>\n\n\n\n<li>Se env\u00eda un email de bienvenida<\/li>\n\n\n\n<li>Se genera un PDF de confirmaci\u00f3n<\/li>\n\n\n\n<li>Se notifica a un webhook externo<\/li>\n\n\n\n<li>Finalmente se devuelve un <code>HTTP 200<\/code><\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">El problema es que todas esas operaciones ocurren de forma secuencial, dentro de la misma request:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Usuario espera...\n\u251c\u2500\u2500 Guardar usuario\n\u251c\u2500\u2500 Enviar email\n\u251c\u2500\u2500 Generar PDF\n\u251c\u2500\u2500 Llamar webhook\n\u2514\u2500\u2500 Respuesta HTTP<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Aunque cada tarea tarde \u00absolo\u00bb algunos cientos de milisegundos, el tiempo total se acumula r\u00e1pidamente. Y hay algo todav\u00eda peor: dependes de sistemas externos. SMTP lentos, APIs inestables, timeouts de red\u2026 Todo eso impacta directamente sobre el tiempo de respuesta que experimenta el usuario.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">La soluci\u00f3n: desacoplar las tareas pesadas del ciclo HTTP<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La soluci\u00f3n moderna consiste en separar las tareas pesadas de la request. En lugar de ejecutar todo inmediatamente, la aplicaci\u00f3n:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Guarda la informaci\u00f3n m\u00ednima necesaria<\/li>\n\n\n\n<li>Publica un mensaje en una cola<\/li>\n\n\n\n<li>Devuelve la respuesta HTTP lo antes posible<\/li>\n\n\n\n<li>Un proceso independiente ejecuta el trabajo en segundo plano<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>HTTP Request\n    \u2193\nController \u2192 dispatch()\n    \u2193\nCola \/ Transport\n    \u2193\nRespuesta HTTP inmediata\n\n(En paralelo)\n\nWorker consume la cola\n    \u2193\nHandler ejecuta la l\u00f3gica<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Este patr\u00f3n tiene varios nombres, <em>background jobs<\/em>, <em>message queues<\/em>, procesamiento as\u00edncrono, pero todos describen la misma idea: el usuario no tiene por qu\u00e9 esperar mientras el servidor genera un PDF o llama a una API externa.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En el ecosistema Symfony, el componente est\u00e1ndar para implementar este patr\u00f3n es <strong>Symfony Messenger<\/strong>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Las piezas del sistema<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Messenger se construye sobre cuatro conceptos fundamentales que conviene entender antes de escribir una sola l\u00ednea de configuraci\u00f3n.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Message<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">El mensaje es un objeto simple que contiene exclusivamente los datos necesarios para ejecutar una tarea. Nada m\u00e1s.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/Message\/SendWelcomeEmail.php\n\nnamespace App\\Message;\n\nfinal class SendWelcomeEmail\n{\n    public function __construct(\n        public readonly int $userId,\n    ) {}\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Una regla importante: los mensajes deben contener <strong>IDs<\/strong> y <strong>datos m\u00ednimos<\/strong>, no entidades Doctrine completas ni servicios. \u00bfPor qu\u00e9? Porque los mensajes se serializan para guardarse en la cola. Pasar una entidad completa implica serializar todo el grafo de objetos, lo que genera mensajes enormes, fr\u00e1giles y dif\u00edciles de versionar. Un simple <code>$userId<\/code> es suficiente: el handler recuperar\u00e1 los datos frescos cuando procese el mensaje.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Message Bus<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">El bus recibe el mensaje y decide qu\u00e9 hacer con \u00e9l seg\u00fan la configuraci\u00f3n.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$bus->dispatch(new SendWelcomeEmail($userId));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">El controlador que ejecuta esta l\u00ednea no sabe d\u00f3nde se procesar\u00e1 el mensaje, cu\u00e1ndo ni qui\u00e9n lo har\u00e1. Eso es precisamente el desacoplamiento que buscamos.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Message Handler<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">El handler contiene la l\u00f3gica real de negocio. Symfony lo detecta autom\u00e1ticamente y lo asocia al mensaje correspondiente gracias al atributo <code>#[AsMessageHandler]<\/code> y al tipo del argumento de<code> __invoke<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/MessageHandler\/SendWelcomeEmailHandler.php\n\nnamespace App\\MessageHandler;\n\nuse App\\Message\\SendWelcomeEmail;\nuse Symfony\\Component\\Messenger\\Attribute\\AsMessageHandler;\n\n#&#91;AsMessageHandler]\nfinal class SendWelcomeEmailHandler\n{\n    public function __invoke(SendWelcomeEmail $message): void\n    {\n        \/\/ Aqu\u00ed va la l\u00f3gica: recuperar el usuario y enviar el email\n    }\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Transport<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">El transporte define d\u00f3nde se almacenan los mensajes mientras esperan ser procesados. Es la pieza que determina si la ejecuci\u00f3n es s\u00edncrona o as\u00edncrona, y qu\u00e9 tecnolog\u00eda de cola se usa por debajo.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Instalaci\u00f3n<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>composer require symfony\/messenger<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Configurando el transporte<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Ejecuci\u00f3n s\u00edncrona vs. as\u00edncrona<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Por defecto, sin configurar ning\u00fan transporte, Messenger ejecuta el handler inmediatamente dentro de la misma request. Esto es \u00fatil para desarrollo y debugging, pero no elimina el bloqueo.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En modo as\u00edncrono, el mensaje se guarda primero en el transporte y el worker lo procesa despu\u00e9s, de forma completamente independiente a la request original.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Doctrine como punto de partida<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Para empezar, Doctrine suele ser la mejor opci\u00f3n: no requiere RabbitMQ, Redis ni ning\u00fan servicio externo. Solo necesitas la base de datos que ya tienes.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># config\/packages\/messenger.yaml\n\nframework:\n    messenger:\n        transports:\n            async: '%env(MESSENGER_TRANSPORT_DSN)%'\n\n        routing:\n            'App\\Message\\SendWelcomeEmail': async<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>MESSENGER_TRANSPORT_DSN=doctrine:\/\/default<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Luego crea la tabla donde se almacenar\u00e1n los mensajes pendientes:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>php bin\/console messenger:setup-transports<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Esto genera la tabla <code>messenger_messages<\/code> en tu base de datos. Cada mensaje pendiente queda all\u00ed hasta que el worker lo consuma.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Despachando desde un controlador<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>namespace App\\Controller;\n\nuse App\\Message\\SendWelcomeEmail;\nuse Symfony\\Component\\HttpFoundation\\Response;\nuse Symfony\\Component\\Messenger\\MessageBusInterface;\n\nfinal class RegisterController\n{\n    public function __invoke(MessageBusInterface $bus): Response\n    {\n        $userId = 42;\n\n        $bus->dispatch(new SendWelcomeEmail($userId));\n\n        return new Response('Usuario registrado');\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Desde el punto de vista del usuario, la respuesta es inmediata. El email se enviar\u00e1 despu\u00e9s, en segundo plano.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Ejecutando el worker<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">El worker es el proceso que consume mensajes de la cola y ejecuta los handlers:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>php bin\/console messenger:consume async<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">En producci\u00f3n, este comando debe ejecutarse continuamente. Para garantizar que se reinicia autom\u00e1ticamente ante fallos, necesitas un supervisor de procesos como <strong>Supervisor<\/strong>, <strong>systemd<\/strong>, o la orquestaci\u00f3n que uses (Docker, Kubernetes).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Resiliencia y fallos<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Reintentos autom\u00e1ticos<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Una de las mayores ventajas de Messenger es su sistema de reintentos. Si un handler lanza una excepci\u00f3n, porque el servidor SMTP no responde, porque una API externa est\u00e1 ca\u00edda, porque hay un problema de red, Symfony puede reintentar el mensaje autom\u00e1ticamente, con un intervalo configurable entre intentos.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Esto convierte errores temporales en algo manejable, en lugar de algo que simplemente se pierde.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Failed Transport: no perder mensajes importantes<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Pero \u00bfqu\u00e9 ocurre si un mensaje falla de forma permanente, incluso despu\u00e9s de varios reintentos? Symfony permite moverlo a una cola de errores separada:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>framework:\n    messenger:\n        failure_transport: failed\n\n        transports:\n            async: '%env(MESSENGER_TRANSPORT_DSN)%'\n            failed: 'doctrine:\/\/default?queue_name=failed'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Desde ah\u00ed puedes inspeccionarlos y reintentarlos manualmente:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>php bin\/console messenger:failed:show\nphp bin\/console messenger:failed:retry<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Esto evita perder trabajos importantes silenciosamente. Un sistema async que no gestiona los fallos no est\u00e1 listo para producci\u00f3n.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Idempotencia: el detalle que marca la diferencia<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Cuando trabajas con reintentos, debes asumir que un mensaje puede ejecutarse m\u00e1s de una vez. Un email podr\u00eda enviarse duplicado. Una API podr\u00eda recibir dos llamadas. Un webhook podr\u00eda procesarse dos veces.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Por eso los handlers deber\u00edan ser <strong>idempotentes<\/strong> siempre que sea posible: ejecutar el mismo mensaje dos veces no deber\u00eda romper el sistema ni producir efectos no deseados.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Este detalle separa los sistemas async \u00abde demo\u00bb de los sistemas realmente preparados para producci\u00f3n.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Qu\u00e9 tareas son buenas candidatas<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Messenger resulta ideal para:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Env\u00edo de emails y notificaciones push<\/li>\n\n\n\n<li>Generaci\u00f3n de PDFs o procesamiento de im\u00e1genes<\/li>\n\n\n\n<li>Llamadas a webhooks y APIs externas<\/li>\n\n\n\n<li>Exportaciones masivas de datos<\/li>\n\n\n\n<li>Sincronizaci\u00f3n con servicios de terceros<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">En general, cualquier tarea que dependa de red, tarde demasiado o no sea cr\u00edtica para responder al usuario de inmediato es una buena candidata.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Lo que <strong>no<\/strong> conviene mover a una cola: validaciones cr\u00edticas previas a la persistencia, l\u00f3gica que debe ejecutarse de forma inmediata, o tareas tan simples que el overhead de la cola no tiene ning\u00fan sentido. El objetivo no es \u00abhacer todo async\u00bb. El objetivo es desacoplar correctamente.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Observabilidad: lo que nadie menciona hasta que falla<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Cuando introduces workers, introduces nuevos puntos de fallo que no exist\u00edan antes. Un escenario com\u00fan: el worker se cae en producci\u00f3n a las 3 de la ma\u00f1ana, los mensajes empiezan a acumularse en la cola, y nadie se entera hasta que un usuario escribe para preguntar por qu\u00e9 no recibi\u00f3 su email de confirmaci\u00f3n hace seis horas.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Conviene monitorear activamente:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Workers detenidos o no respondiendo<\/li>\n\n\n\n<li>Colas creciendo indefinidamente<\/li>\n\n\n\n<li>Mensajes fallidos acumul\u00e1ndose<\/li>\n\n\n\n<li>Tiempos de procesamiento fuera de lo normal<\/li>\n\n\n\n<li>Retries excesivos que indican un problema sist\u00e9mico<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Una cola silenciosamente bloqueada puede convertirse r\u00e1pidamente en un problema operativo serio.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Apliquemos asincron\u00eda<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Symfony Messenger ofrece una forma elegante y robusta de introducir procesamiento as\u00edncrono en aplicaciones PHP modernas. Su mayor ventaja no es solo mejorar el rendimiento: es mejorar la arquitectura. Separar responsabilidades, reducir acoplamiento, eliminar bloqueos innecesarios en las requests HTTP.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Lo mejor es que puedes empezar de forma extremadamente simple con Doctrine, sin incorporar infraestructura compleja desde el primer d\u00eda. Cuando el sistema crezca y lo necesites, migrar a Redis, RabbitMQ o Amazon SQS no requerir\u00e1 tocar la l\u00f3gica de negocio de tus handlers. Ese desacoplamiento es precisamente lo que hace de Messenger una herramienta tan potente dentro del ecosistema Symfony.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Cuando una aplicaci\u00f3n PHP empieza a crecer, uno de los primeros problemas de rendimiento suele aparecer en el peor lugar posible: la experiencia del usuario. Un registro que tarda demasiado.Una compra que demora varios segundos.Una API que parece \u00abcongelarse\u00bb antes de responder. En muchos casos, el problema no es PHP en s\u00ed. El problema es [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[15],"class_list":["post-329","post","type-post","status-publish","format-standard","hentry","category-desarrollo","tag-php"],"_links":{"self":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/329","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/comments?post=329"}],"version-history":[{"count":0,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/329\/revisions"}],"wp:attachment":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/media?parent=329"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/categories?post=329"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/tags?post=329"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}