- engineering
- ussd
- connectivity
Building for Intermittent Connectivity: Lessons from USSD
By Sente Team · 16 October 2026 · 6 min read
Most fintech products are designed on a fast laptop, tested on a recent smartphone over office Wi-Fi, and launched for people who will use them on neither. Across much of Africa, the phone in someone's pocket is a basic handset, the network drops to 2G at the edge of town, and data costs money that has to be weighed against food and transport.
That is why USSD — the menus you reach by dialling a code like *384# — is still the most widely used digital money channel on the continent. It works on every phone ever sold, needs no data bundle and no app install, and runs over the signalling channel that keeps working when mobile data doesn't.
It is also unforgiving to build for. This article covers what we learned designing Sente's USSD channel, and the engineering choices that make it fast and safe.
What makes USSD hard
USSD is a session: the phone and the network hold a short-lived conversation, one screen at a time. Each screen is a round trip from the handset, through the mobile operator's USSD gateway, to our service and back.
Three properties shape everything:
- Sessions are short and strict. Operators end a session if a response takes too long — often within seconds — and some cap the total session length. A slow screen doesn't just feel bad; it ends the conversation.
- Sessions drop. A user walks behind a building, the network hands over between towers, a call comes in. The session ends wherever it was, without warning.
- Screens are tiny. Around 160 characters per screen, plain text, numbered options. There is no room for explanations, error messages need to be a single line, and every extra step costs time.
Put together: every response has to be fast, every step has to survive being interrupted, and nothing a user does twice should ever move money twice.
Our approach: menus in memory, money only on confirmation
We model each USSD session as a finite state machine whose state lives in Redis, not in the relational database.
As a user moves through the menus — choose Send money, enter a phone number, enter an amount, review — each step updates a small piece of session state in memory. No payment is created, no balance is touched, no row is written to Postgres. Navigation is just a state transition, and reading or writing it takes a fraction of a millisecond.
Only when the user reaches the final confirmation screen and enters their PIN does the session turn into a payment request, and only then does the request go to the core platform — through the same checks, holds and ledger postings as a payment from the app.
Why it matters
- Speed. With no synchronous database writes during navigation, our service adds very little to each round trip. Our target is under 100 milliseconds per screen inside our own systems, which leaves the operator's network the rest of the budget.
- Nothing half-done. If the session drops on the amount screen, nothing has happened. There is no orphaned draft payment to clean up, no reserved funds stuck in limbo.
- Simplicity. The state machine defines exactly which screen can follow which. Unexpected input — a letter where a number belongs, an option that doesn't exist — leads to a defined state with a one-line message, never an exception.
Surviving the dropped session
The hardest moment is the very end. A user confirms a payment, the request reaches us, the payment succeeds — and the session drops before the confirmation screen reaches the phone. The user didn't see "Sent". Did it work? The natural reaction is to dial again and try once more.
Without protection, that second attempt is a second payment. This is where idempotency keys come in.
When a USSD session reaches confirmation, it carries a unique key, generated once for that intent and stored with the session. The core platform records the result of the first request against that key. If the same intent arrives again, it gets the original result back instead of a new payment. The money moves once, however many times the network makes us ask.
We then make it easy to find out what happened. The main menu has a Mini statement option that shows the last five transactions, so someone who re-dials can check whether the payment went through before deciding to try again — and if they do try again with the same intent, the key still protects them.
Designing for 160 characters
Speed is only half the experience. The other half is what fits on the screen.
- Numbers, not words, for choices. Every option is a single digit. Typing is slow on a keypad, and a one-digit reply is fast and hard to get wrong.
- The most common task first. Send money is option 1 on the main menu; airtime, bundles, bills, balance and the mini statement follow. Settings live one level down, under My account.
- Review before you commit. The last screen before the PIN prompt repeats who the money is going to and how much, so a mistyped number or an extra zero is caught before anything moves.
- Short, specific errors. "Amount too high for your daily limit" tells someone what happened and what to do. "Transaction failed" does not.
- The user's language. People can switch the menus between English, French, Kiswahili and Portuguese from My account, and we keep adding languages.
Security on a phone with no screen lock
A basic phone often has no PIN of its own and may be shared within a family. USSD security has to assume the handset is not private.
- The PIN is entered on the last screen only and is never stored or logged in plain text anywhere in our systems. Only a hash is checked.
- Payments are checked for a recent SIM swap — one of the most common fraud patterns in mobile money. If the SIM was just replaced, the payment is stopped and the user is told why.
- Requests are rate-limited, so a stolen phone can't be used to hammer the menus.
- Every payment from USSD goes through the same fraud scoring and compliance checks as any other channel. There is no "lighter" path for a cheaper phone.
One platform behind every channel
USSD is not a separate product with its own balances and its own rules. It is one more way into the same platform. A payment made by dialling a code posts to the same double-entry ledger as one made in the app; it appears in the app history the moment it posts; and the same customer support team can see it.
That matters for people who use both — a trader who checks her balance by USSD in the market and reviews her month in the app at home — and it matters for us, because every fix and every safeguard we build applies to everyone at once.
What we learned
A few principles carried over from USSD into everything else we build:
- Design for the worst network first. If it works on a dropped 2G session, it works on fibre.
- Separate browsing from committing. Let people explore freely; make the moment money moves explicit, single and protected.
- Assume every request will be retried. Idempotency is not an optimisation; it is the only way to be sure.
- Make it easy to check what happened. A quick way to see recent activity is the difference between confidence and a panicked second attempt.
A feature phone that costs less than a week's groceries deserves a payment experience as dependable as the newest smartphone. That is the bar we hold USSD to.