Messenger that charged the card twice
The provider returned 200. The worker died before ack. Redis redelivered. Unique middleware did not cover SIGKILL.
The shop used Symfony Messenger on Redis next to a Laravel catalog on Horizon. Checkout dispatched PayOrder. The handler called the provider, then acked. Docker sent SIGTERM on deploy. One worker had a 200 and no ack. The next consumer charged again.
Messenger unique middleware was on. It serializes overlapping consumers. It does not survive a crash after the HTTP call. Support saw two provider ids for one order. The customer saw two charges.
The problem was a charge without a durable fence. Unique middleware is not a ledger. I needed an insert with unique (order_id, step) before HTTP, then ack. Replay must be empty.
Insert before HTTP
A table with unique (order_id, step) is the fence. Insert in a transaction before the provider call. On conflict, load the stored provider_ref, skip the charge, ack. Three retries, then the failure transport. Timeout shorter than the Redis reservation.
Same Redis, two workers
Horizon and Messenger shared one instance. Prefixes split jobs from cache and sessions. The fence lives in MySQL, not in a Redis SET NX that dies with maxmemory. The Laravel queue on this site uses the same table idea. Symfony was not special. The missing insert was.
What I took from this
Ack after HTTP is a window. SIGTERM in that window is a second charge unless the fence is already in the database.
Unique middleware serializes consumers. It does not survive SIGKILL. A unique row does.
Money steps go in MySQL. Redis is the transport. The two jobs must not share a lock that maxmemory can drop.
