← ALL POSTS
9 MARCH 2026

An OTP Is Not A Newsletter: Building An Adaptive Retry Ladder For SMS

Standard advice for a failed send is exponential backoff. Wait a bit, wait longer, wait longer again. It is good advice for most things and it is completely wrong for a one-time code.

If someone is sitting on a login screen waiting for a six-digit code, a thirty second backoff is a lost customer. They have already hit "resend" twice and started muttering.

But the same gateway also carries cart-recovery messages and marketing sends, where a thirty second wait is irrelevant and hammering the server is worse than waiting. One backoff strategy cannot serve both.

So we wrote our own.

The ladder

A custom Bull strategy, sms-adaptive, thirteen attempts across about five and a half minutes:

Attempts Interval Covers
1 to 4 every 3s the first 12 seconds. OTPs, live bot conversations
5 to 7 every 10s the next 30 seconds. Short connectivity blips
8 to 10 every 30s the next 90 seconds. Moderate problems
11 to 13 every 60s the next 3 minutes. Extended problems

The shape is the point. It is aggressive exactly where it matters, in the window where a human is still looking at their phone, and then it backs right off so a genuinely offline handset does not get pounded for five minutes.

Before this we had two attempts at one second apart, then later five attempts on a thirty second exponential. The first version gave up while the phone was still waking up. The second version was useless for codes.

Layer two: the heartbeat sweep

Retries only help if the process is still trying. A handset that goes flat, or goes in a pocket in a dead spot, or gets put in flight mode on the way to Sydney, blows straight through thirteen attempts.

So the device heartbeats. If it comes back after being away more than two minutes, the API automatically retries every failed or unknown message for that device from the last 24 hours.

We also stretched the status expiry window from twenty minutes to twenty-four hours for the same reason. A twenty minute window means an overnight-offline phone wakes up to a pile of messages marked unknown forever, with nothing to sweep them up.

There is an Android side to this too: a connectivity-change receiver so the phone heartbeats the instant it gets signal back, rather than waiting for the next scheduled one.

Layer three: a human with a button

Automatic retries handle transient problems. They do not handle "the SIM ran out of credit on Friday and nobody noticed until Monday".

So the dashboard got a status filter, a retry button per message, a "retry all failed" for a device, and a retry inside the message detail dialog. Plus API endpoints for the same, because sometimes you want to do it from a script.

What I would tell anyone building this

Work out what the message is for before you choose a backoff. Everything else follows from that. A code, a conversation reply, and a promo all want different behaviour, and if you only implement one you have picked a winner without meaning to.

Assume the device is the unreliable part. It is a phone. It will be flat, in a pocket, on a plane, or updating itself. The retry ladder handles seconds and minutes. The heartbeat sweep handles hours. Build both.

Make failures visible. The retry ladder is not the important bit. The important bit is the dashboard telling you four messages failed, because a silent failure is a support ticket you get three days late from an angry client.

Want this working on your store? Aiva does the sales, marketing and service work - in your voice, around the clock.
Get Aiva