Managing USSD Session State
USSD gateways are stateless: each user keypress arrives as a fresh HTTP request. Your service must reconstruct where the user is in the flow. This guide covers tracking state and keeping sessions continuous.
๐ The session_idโ
Every request carries a session_id that stays constant for the whole interaction. Key all your state on it.
{ "session_id": "ussd_abc123", "phone": "+254712345678", "text": "1*2" }
๐งต Two Ways to Track Stateโ
| Approach | How | Trade-off |
|---|---|---|
Concatenated text | Gateway sends full input history (1*2*50) | No storage, but fragile for long flows |
| Server-side store | Save state keyed by session_id | Robust, needs a store with TTL |
For anything beyond a couple of steps, use a server-side store.
๐งฉ Parsing Concatenated Inputโ
Many gateways append each entry with *. The last segment is the newest input.
def handle(req):
steps = req['text'].split('*') if req['text'] else []
if not steps:
return main_menu()
if steps[0] == '1':
return "END Your balance is KES 240."
if steps[0] == '2':
return buy_airtime(steps[1:])
return "END Invalid option."
๐พ Server-Side State with TTLโ
Store progress under the session ID and expire it after the session window.
async function handle({ session_id, text }) {
let state = (await store.get(session_id)) || { step: 'menu', data: {} };
if (state.step === 'menu') {
if (text === '2') { state = { step: 'amount', data: {} }; await store.set(session_id, state, 180);
return 'CON Enter amount:'; }
}
if (state.step === 'amount') {
state.data.amount = text;
await store.del(session_id); // session complete
return `END Sending ${text}. Confirmation sent by SMS.`;
}
return 'CON 1. Balance\n2. Send money';
}
๐ Continuity & Cleanupโ
- Set a TTL matching the carrier session window (~180s) so stale state self-clears.
- Delete state on
ENDto free storage and avoid stale resumes. - Don't rely on memory โ use a shared store so any instance can serve the next keystroke.
- Treat re-dials as new sessions โ a new
session_idmeans start over.
๐ก Best Practicesโ
- Always key on
session_id, never on phone number alone (concurrent sessions). - Persist minimal data โ only what the next step needs.
- Expire aggressively to match gateway timeouts.
- Validate each step's input before advancing state.
- Make handlers idempotent in case the gateway retries.
๐ Related Resourcesโ
Last Updated: May 2026