A Persistência É O Caminho Do Êxito - A persistência é o caminho do êxito. Charles Chaplin - Pensador
A persistência é o caminho do êxito. Charles Chaplin - Pensador

Why everyone gets persistence wrong in production

Most people think persistence is about retrying the same thing over and over until it works. That is the kind of thinking that takes down services at 3 AM. The real pattern is a lot more boring and a lot harder to implement correctly. When I say a persistência é o caminho do êxito, I am not talking about throwing more CPU at a flaky dependency. I am talking about knowing exactly which failures are transient, which are permanent, and how long to wait between retries without making your users wait forever.

I spent six months dealing with a payment gateway integration that would fail randomly on about 4% of requests. The first attempt was to just retry with a fixed delay. That got us from a 4% failure rate to a 12% latency problem. Users were getting stuck in loading spinners while our backend hammered their servers with exponential backoff like an idiot. The workaround was to layer the retries: an immediate fire-and-forget retry on the server side for idempotent operations, a visible spinner with a hard timeout on the client side, and then a database queue that processed the remainder asynchronously with deduplication keys. It cut the user-visible failure rate to under 0.3% and the average latency impact to about 200 milliseconds for the affected requests.

👉 Clique no botão abaixo para saber mais sobre o assunto!

a persistência é o caminho do êxito, mas só quando você sabe qual tipo de persistência usar

There are three distinct patterns people confuse with each other, and using the wrong one is why your system feels unreliable even though it has retries. Retry loops are for transient errors. Network blips. Timeout anomalies. Database lock contention that resolves in milliseconds. The key detail everyone misses is that you need to classify the error before you retry. A 500 from a payment processor is not the same as a 500 from your own upstream. One you retry. The other you log and escalate. Exponential backoff with jitter is the standard, but it has a well-known trap called thundering herd. When all your instances simultaneously fail and all simultaneously retry, you amplify the load on the broken dependency. Add a random jitter component to each delay, and you spread the retry attempts across a time window. This is not a nice-to-have, it is a requirement for anything with more than one instance. Dead letter queues are for the failures you give up on. Sometimes the right answer is not to retry but to store the payload somewhere safe and process it later with a human in the loop. I once had a system where we auto-retried address validation failures for two weeks. We ended up re-processing thousands of addresses that had been invalid because the user entered their apartment number wrong. The fix was to stop auto-retrying after three attempts and route those records to a manual review pipeline instead. We recovered maybe 15% of them, but we saved a ton of unnecessary API calls.

When persistence makes things worse

This is the part nobody writes about in blog posts. Persistence is destructive when applied to the wrong class of problems. If you persistently query a cache that is already saturated, you are making the outage worse, not better. If you persistently write to a database that is hitting its write limit, you are contributing to the stall. The correct behavior in those scenarios is to fail fast and degrade gracefully, not to keep hammering. I had a microservice that hit this exact wall during a traffic spike last year. It was configured with a 30-second timeout and five retries with backoff. Under normal conditions it was fine. When the downstream cache service started dropping connections, our service consumed all available connections in its pool and became completely unresponsive. The retry logic was the problem, not the symptom. We reduced the timeout to 5 seconds, cut retries to two, and added a circuit breaker that stopped calling the service entirely after three consecutive failures. The error rate on our end went up slightly because some requests failed outright instead of eventually succeeding, but the overall system stability improved dramatically because we stopped amplifying the downstream problem.

The counter-intuitive insight here is that sometimes the most resilient behavior is to give up quickly. A user seeing "try again later" is better than a user stuck in a loading state while your infrastructure collapses under retry load.

Practical implementation details

If you are building this from scratch, start with the error classification. This is the step that separates systems that scale from systems that break under pressure. Tag each failure as transient, permanent, or unknown. Transient gets retried. Permanent gets logged and surfaced. Unknown gets a conservative retry with a short timeout and a fallback path. For the retry mechanism itself, use an existing library rather than writing your own. You will get the jitter, the backoff curve, and the max retry count right by accident when you use something battle-tested. Rolling your own introduces subtle bugs like integer overflow on delay calculations or edge cases where the retry count never actually terminates. Idempotency is non-negotiable. If your retry can cause a duplicate action, you have not solved the problem, you have created a new one. Every persistent operation that touches money, inventory, or user data needs an idempotency key. This is usually a UUID generated by the client and sent with the request. Your backend stores the key and returns the cached result if the same key arrives twice. The deduplication store should be fast. A Redis key with a TTL is overkill for most cases but sufficient. For high-throughput systems, a local in-memory set with periodic flush to persistence works well because the deduplication window is usually measured in seconds, not hours.

What I wish I knew before building this

Monitoring retry behavior is as important as the retries themselves. I would track the retry count distribution, the error type breakdown per retry attempt, and the latency impact of retries versus immediate failures. Without these metrics you are flying blind and making configuration decisions based on gut feeling. The hardest part is tuning the timeouts. A timeout that is too short causes unnecessary failures. A timeout that is too long causes resource exhaustion. The sweet spot is usually shorter than you think. Most operations that appear to be slow are actually just stuck, and a fast timeout plus a retry is better than a long timeout plus hope.

There is also the question of what happens when everything retries at once after a global outage. This is called the stampede problem and it is the reason you need staggered or randomized retry initiation, not just randomized backoff delays. A simple approach is to add a small random initial delay before the first retry attempt, so not all instances wake up and retry at the exact same moment after a failover.

The reality is that persistence is a tool, not a philosophy. Using it everywhere makes your system fragile. Using it judiciously with proper classification and fallbacks makes it robust. The difference between a reliable system and a broken one is usually not whether you implemented retries, but whether you thought about what happens when the thing you are retrying against is itself in a bad state.