I rewrote our generated RabbitMQ client from Actix actors to plain Tokio. The old version worked, so this was not a crusade against actors. It was a response to a message path that had collected more machinery than the problem needed and had become unpleasant to reason about during nightly incidents.
The message path had too much machinery
A delivery passed through supervisors, actor mailboxes, boxed handlers, futures tied to actor contexts and several layers of code before reaching a job that was basically: read the message, route it, run the handler and acknowledge it.
Keep the hot path visible
The new version keeps those steps visible. Ready connections are read through ArcSwap, so the hot path does not need a shared write lock. Reconnects pass through one gate: the first task that notices the broken connection performs the work, while the others wait and recheck instead of creating a small reconnect festival.
Bound the work before it bounds you
Consumers run as a bounded worker pool. A semaphore limits in-flight messages to the prefetch size, a JoinSet owns active handlers and shutdown gets priority over accepting more work. I still have typed, schema-generated clients; I simply removed the actor layer from the route between RabbitMQ and the handler.
Batch the confirms
The producer improved most after batching publisher confirms. A single publish still waits for its confirm, but a batch publishes several messages and waits once. That amortises the round-trip instead of paying for it message by message.
What changed in production
After rollout, consumer throughput improved by about 20%, producer throughput by roughly two to three times in messaging-heavy workloads, memory dropped by 30–40% under heavy traffic and by a few megabytes per idle pod, and removing the Actix dependency tree cut service build time by around 10%.
Benchmarks need context
The database is still the main bottleneck. I am mentioning that because "producer 3x faster" does not mean the complete pricing platform became three times faster. It means one important layer stopped adding as much latency and drama.
The best metric was silence
The best production result is less glamorous than the benchmark: recurring messaging-related alerts dropped to effectively zero.
Tokio did not win a framework war. I ended up with a generated client that is easier to inspect, bounded under load and no longer the thing waking me up at night. That is a victory I am happy to take.