Designing Resilient APIs for Flaky Mobile Networks in Africa
A payment confirmation that never arrives. A form submission that fires twice because the user tapped the button again after a timeout. A ride-hailing app that crashes mid-booking when the network drops between a tower handoff. These are not edge cases for software teams building in Africa — they are the default failure mode.
Most API design literature is written with a 50ms round-trip and a stable 4G or fiber connection in mind. That assumption quietly bakes brittleness into systems that will be deployed across West and East Africa, where 2G and 3G connections still account for a significant share of mobile traffic, and where latency can spike unpredictably even on nominally "4G" networks in dense urban areas like Lagos, Nairobi, or Accra.
Building for this reality is not about lowering your standards. It is about raising them.
The Failure Modes You Are Actually Dealing With
Before reaching for solutions, it helps to name the actual network conditions your API clients face:
- High and variable latency: Round-trip times that swing between 300ms and 4000ms on the same connection within minutes.
- Packet loss: Requests that never reach your server, or responses that never reach the client.
- Interrupted connections: Mid-request drops during tower handoffs or when a user moves between coverage zones.
- Asymmetric bandwidth: Upload speeds significantly lower than download speeds, which matters for any API that accepts file uploads or large POST bodies.
- Data cost sensitivity: Users on prepaid data plans who pay per megabyte — meaning bloated API responses have a real financial cost to the person using your product.
Each of these failure modes demands a specific design response.
Exponential Backoff With Jitter
When a request fails, the worst thing a client can do is immediately retry at full speed. On a congested or intermittent network, a flood of retries from thousands of devices compounds the problem and can tip a struggling API server into a full outage.
The correct pattern is exponential backoff with jitter: after each failed attempt, wait for an interval that doubles (or grows by some factor), then add a small random offset to desynchronise retries across clients.
A simple implementation in JavaScript:
async function fetchWithBackoff(url, options, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response;
} catch (err) {
if (attempt === maxRetries - 1) throw err;
const base = Math.min(1000 * 2 ** attempt, 30000);
const jitter = Math.random() * 1000;
await new Promise(res => setTimeout(res, base + jitter));
}
}
}
Critically, backoff logic belongs on the client, but your API should signal retryability correctly. Return 503 Service Unavailable or 429 Too Many Requests with a Retry-After header when appropriate. Never return 500 for a condition that is safe to retry — that misleads client implementations.
Idempotency Keys: Your Safety Net for Duplicate Requests
Interrupted connections create a deceptively dangerous scenario: the server processed the request, but the client never received the confirmation. The user retries. The action fires twice.
For operations with side effects — payments, order submissions, account creation — this is unacceptable. The solution is idempotency keys.
The client generates a unique key (a UUID is fine) and includes it in the request header: Idempotency-Key: <uuid>. The server caches the result of the first successful execution, keyed by that value. Any subsequent request with the same key returns the cached result without re-executing the operation.
A few implementation rules that teams routinely get wrong:
- Store idempotency results in a persistent cache (Redis with TTL is typical), not in-memory. Restarts should not invalidate keys.
- Lock on the key before executing, not after. Without a distributed lock, two simultaneous requests with the same key can both execute before either writes to the cache.
- Return exactly the same HTTP status code and response body on a replayed request. Do not return
200the first time and409 Conflicton a replay — that breaks client logic. - Set a sensible TTL. Twenty-four hours covers the realistic retry window for most mobile use cases without growing your cache unboundedly.
Stripe's API popularised this pattern. If you are building a payments or financial services product in Africa — and many of Code!nk's clients are — this is non-negotiable infrastructure.
Partial Responses and Sparse Fieldsets
Bandwidth costs money in Africa. A mobile user in Ghana or Nigeria is often on a prepaid plan paying real money per megabyte. An API that returns a 40-field user object when the client needs three fields is not just inefficient — it is extracting cost from your users.
Design your APIs to support sparse fieldsets: a query parameter (e.g., ?fields=id,name,avatar_url) that limits the response to only the requested fields. This pattern is formalised in the JSON:API specification and is straightforward to implement in any REST framework.
Beyond fieldsets, consider pagination defaults that match the network context. A default page size of 100 records might be acceptable on broadband but punishing on 2G. Default to smaller pages and let clients explicitly request larger ones.
For read-heavy endpoints, HTTP conditional requests (ETag + If-None-Match, or Last-Modified + If-Modified-Since) allow clients to confirm that cached data is still fresh without re-downloading the full payload. A 304 Not Modified response can save hundreds of kilobytes per session for a list-heavy app.
Designing for Offline-First Client Behaviour
Resilient APIs do not exist in isolation — they enable resilient clients. When your API surface is designed with clear idempotency guarantees, well-defined error semantics, and explicit cache headers, client developers can build offline-first experiences: queue mutations locally, sync when connectivity resumes, and show users a coherent UI state throughout.
This is the architecture behind the best-performing apps in the African market. The API's job is to make that client-side queuing and reconciliation as safe and predictable as possible.
Compression Is Not Optional
Enable gzip or Brotli compression on every API response. This is a one-line configuration change in most frameworks and can reduce JSON payload sizes by 60–80%. Brotli achieves slightly better compression than gzip and is supported by all modern mobile browsers and HTTP clients. There is no valid reason to ship an uncompressed API in 2025.
Additionally, consider Protocol Buffers or MessagePack for high-frequency endpoints where binary serialisation over JSON yields meaningful bandwidth savings — though for most teams, compressed JSON is sufficient.
Why This Matters for Your Project
If you are building a SaaS product, a fintech platform, or a consumer mobile app intended for African users, network resilience is not a post-launch optimisation — it is a core product quality dimension. Users on constrained networks do not file bug reports; they quietly abandon your product. Designing your API around idempotency, intelligent retry semantics, lean payloads, and caching headers is the difference between a product that works for your actual users and one that only works in a demo environment.




