USSD timeouts and retries: what happens while a caller waits
September 20, 2026
A USSD session is not a web request. The operator holds it open, the caller stares at a blank handset while you think, and the budget you get is measured in single-digit seconds. Almost every USSD outage anyone has ever told us about comes back to that sentence.
This is what actually happens in those seconds, and what to do about it.
The shape of a session
Each screen a caller sees is one request from the gateway to your callback, and one answer from you. The session id ties them together. You answer with the text to show and whether the session continues or ends; nothing reaches the handset until you answer.
-
Handset
-
Operator network
-
Aggregator or gateway
-
Your flow
Every hop spends some of the budget. What is left is yours, and your API.
What arrives differs by who you are connected to, and the difference matters more than it looks:
Africa's Talking sessionId, phoneNumber, serviceCode, text
MTN sessionid, msisdn, input, isnewrequest
Zamtel TransId, MSISDN, USSDString, RequestType
Africa's Talking sends text as the whole conversation so far, joined by * — 1*1234*2. The operator gateways send only what the caller just typed. So on Africa's Talking you can rebuild the caller's position from the request alone, and on MTN or Zamtel you cannot: you hold that state, or you lose it.
Notice what is missing from all three: there is no per-request id. Only a session id. That has consequences further down.
The budget is the caller's patience minus the network's
Every hop takes some of it: the handset to the network, the network to the gateway, the gateway to you, and all of it back. What is left is yours, and your API's.
The exact budget differs by operator and by the agreement you signed. Ask for the number in writing, then measure your own p99 rather than your average — the average is fine on every service that has ever fallen over. A menu whose balance lookup usually takes 300 ms and occasionally takes nine seconds is a menu that occasionally loses everyone.
What a timeout actually costs
Here is the part that surprises people, and it is the opposite of the web:
In our experience with MTN and Zamtel, a request that times out is not retried. The gateway does not come back. The session is gone, the handset shows a network error, and the caller is standing in a queue somewhere deciding whether you are worth a second attempt.
So a timeout is not a slow success. It is a lost customer, and your logs will show it as nothing at all unless you went looking.
-
0s
The gateway opens a session and calls you.
-
0.2s
Menu shown. The caller picks "balance".
-
4s
Your account lookup is still waiting on the core banking system.
-
Dropped
The gateway gives up. The handset says the network failed.
A minute later they dial again.
- Lost
sess_4f21→sess_b907- Carries across
+260 97 000 0001
When that caller does redial, the request arrives with a new session id. You cannot correlate it with what they were doing thirty seconds ago — not from the request, anyway. The only thing that carries across a redial is the phone number.
"Retry" means two different things
Because of that, the word does more work in USSD than it can carry.
A gateway re-sending a callback. Some aggregators do this on certain errors. Your handler may therefore see the same session id and the same input twice. If that input was "confirm transfer", the second one must not move money again.
A caller redialling. New session, no connection to the last one, and they expect to pick up where they left off — or at least not to start from a screen that says their transfer is already pending.
A gateway re-sending
The same request reaches you twice.
same session id, same input
Answer from the session record, do not call the API twice.
A caller redialling
They expect to pick up where they left off.
new session id, same msisdn
Offer to carry on, keyed on the number. Nothing else survives.
The first is an idempotency problem. The second is a state problem. Conflating them is how a service ends up either double-charging people or making them start again every time the network hiccups.
Designing for it
- Never put a slow call in a screen the caller is waiting on if you can put it anywhere else. Take the input, end the session with "we are processing this, you will get an SMS", and do the work after.
- Make every write idempotent, keyed on something stable — the session id plus the step, rather than a timestamp or a random id you generate per request.
- Make your state resumable by phone number, because that is the only thing a redial carries. "Continue where you left off, or start again" is not a nicety; it is the recovery path for the most common failure in the channel.
- Count timeouts as lost sessions in your load tests. If your test models a gateway retrying, it is measuring a system nobody runs — and it will overstate double-applied writes while hiding how many callers you dropped.
What luftflow does about it
The engine holds the session and rebuilds a caller's position by replaying their inputs, so the MTN-style gateways that send one keystroke at a time are no harder to serve than Africa's Talking. Every action a flow takes — an API call, a variable set — is recorded in that session's log, and a replay reads the log rather than making the call again. That is what makes a re-sent callback safe: the same input arriving twice is answered from the record of what happened the first time, not by calling your API a second time.
What it does not do for you yet is resume a dropped session on a redial. The phone number is there in the flow, and you can key your own "continue where you left off" screen on it, but luftflow will not offer that to the caller by itself. If you need it, build it into the flow and tell us — it is a good candidate for being built properly into the engine.