OrderPlaced dispatched, 14:02:11. Doctrine rollback, 14:02:11. Two log lines with the same timestamp, and between them a customer who got a confirmation email for an order that does not exist. Most of us have had a version of that night. You save a row and push a message to AMQP or SQS, the two operations are not one atomic thing, and sooner or later the gap between them hurts you. My position on Symfony 8.2 is simple. Out of everything in this release, the transactional outbox matters most to working teams, and I think it should become the default way you wire Messenger to a broker. It shouldn't be the clever thing you reach for after the third incident.
php bin/console messenger:consume db_outbox
# handles the messages sent to "orders" as usual
php bin/console messenger:consume ordersFirst the facts. 8.2 is due at the end of November 2026, needs PHP 8.4.0 or newer and has support until July 2027. The branch is still moving, so don't deploy anything from it yet. The outbox is Nicolas Grekas's work, and it's an option on a transport. You name a Doctrine transport as the outbox, messages are inserted there inside your database transaction, and a relay worker forwards them to the real broker later. Your consumers keep reading from RabbitMQ or SQS exactly as before. A rollback takes the message down with the data. If the broker is down, the message waits in a table until the broker comes back.
We could already get most of this before. Routing everything to the Doctrine transport gave you transactional inserts, and plenty of teams quietly did that. The price was that your database turned into the queue, with every worker polling it. I've watched a modest Postgres box spend a real share of its CPU answering the same empty SELECT from a dozen idle consumers. The outbox keeps the useful part of that trick, the insert that commits or rolls back with your data, and gives the actual queueing back to software built for it.
Here's the strongest objection, and I think it's a fair one. You get another moving part. The relay is a process you have to supervise, monitor and scale, and if it stalls your messages pile up in a table where nobody is looking. Ordering also goes away: the docs say forwarded messages may arrive out of order. Retries get more involved too. A failure to forward uses the outbox transport's retry strategy and failure transport, while a failure in the handler retries on the target transport. That gives you two failure paths to understand instead of one, and anyone who has debugged a retry storm knows each extra path costs something.
I still come down on the side of the outbox, because the alternative only feels simpler since its failures are invisible. A message dispatched before the commit gives you no metric when it goes wrong. You find out from a support ticket a week later. A stalled relay gives you a growing row count you can put an alert on. Given the choice, I'll take the failure I can see. On ordering: if your handlers depend on arrival order across a distributed broker, they were already fragile, and the outbox just makes you admit it. Delays keep working too. A delayed message waits in the outbox and goes out to the broker when it's due.
The same release ships a companion feature that comes from the same honesty about delivery. Yanick Witschi contributed a claim_check option: anything over a byte limit you set per transport goes into a PSR-6 cache pool, and the broker only carries a reference with a checksum. Again you trade an implicit failure for an explicit one. Your cache pool now sits on the delivery path, its default_lifetime has to cover delays, retries and time in the failure transport, and if a claim expires you get a ClaimCheckNotFoundException wrapped in a MessageDecodingFailedException that goes through normal retry handling. That is better than a broker silently rejecting a fat payload, as long as nobody runs cache:pool:clear on the wrong pool.
In practice the change is two workers instead of one. That's the whole operational cost, and I'd much rather explain an extra supervisor entry in code review than explain a phantom order email to a product manager.
So here is what I'd like to hear from you, especially if you run Messenger at volume: would you turn the outbox on for every transport that sits next to a database write, or are there workloads where you would deliberately keep sending directly from inside the transaction and accept the risk? If you'd keep some, I'd like to know which ones and why.




Comments
No comments yet — be the first.
Open the discussion
No account or password needed — just enter your e-mail and we’ll send you a one-time sign-in link. First time here? You’re set up automatically.
Your rating will be applied automatically after you sign in.
Check your inbox
We’ve sent a sign-in link to …. Open it on this device — this tab will sign you in automatically.
Nothing arrived? Check your spam folder — and mark the mail as "Not spam" so it lands in your inbox next time.