Design for variance, not average latency
Measure the distribution: DNS, connection, TLS, request upload, server processing, first byte, download, packet loss, reconnects, and radio transitions. Segment by country, carrier/ASN, network type, app version, device, endpoint, payload size, and attempt. p50 hides the users who repeatedly cross the timeout boundary.
High round-trip timeEvery handshake and sequential dependency is expensive. Reuse connections, remove chatty calls, batch carefully, and parallelize only within capacity.
Loss and route changeRequests stall, connections migrate or reset, and the client cannot infer whether a write committed.
Variable bandwidthA small response succeeds while a media upload or unbounded list fails halfway through.
Intermittent availabilityApps move between online, captive, degraded, background, and offline states; queued intent outlives a process.
Give the request one end-to-end deadline
A client operation needs a deadline, not independent 30-second timeouts at every hop. Reserve time for DNS/connect/TLS, the first attempt, optional retry, response parsing, and UI feedback. Propagate remaining budget downstream so abandoned work stops. Separate connect timeout from request/idle timeout, and cancel server work when the protocol and execution model permit it.
DNS
Connect
TLS
Try 1
Jitter
Try 2
Budget
Parse
UI
Retry only when method, failure, and budget allow it
HTTP defines safe and idempotent method semantics, but application side effects still need care. Retry GET/HEAD when transient and within budget. PUT/DELETE can be idempotent by contract yet still trigger external effects if implemented poorly. Do not automatically retry POST/PATCH unless the API supplies an idempotency contract. Never retry validation, authentication, authorization, conflict, or permanent not-found errors as if they were packet loss.
Capped exponential backoff with full jitterT+0Attempt 1Send with request ID, deadline, and idempotency key when required.
transient failure → random 0..base
T+Δ1Attempt 2Reuse the same logical operation ID; stop if budget or policy no longer permits.
random 0..min(cap, base × 2²)
T+Δ2Final attemptReturn a recoverable state, queue intent, or surface failure. Do not loop indefinitely.
Put retries at one layer
If the mobile SDK retries three times, the gateway retries three times, and the service retries its dependency three times, one action can amplify to 27 downstream calls. Choose the layer with the operation semantics and remaining budget. Use a retry quota or token bucket, cap attempts and delay, apply full jitter, respect Retry-After, and disable retries when failure is sustained. Hedged requests should be limited to safe reads and charged to a separate budget.
| Outcome | Client interpretation | Retry policy | Server contract | Failure to avoid |
|---|
| Connection lost before response | Write outcome may be unknown. | Retry only if safe/idempotent or keyed. | Stable operation status retrievable by key or ID. | Create the purchase twice. |
| 408 / timeout | Request did not complete in the available context. | Bounded retry when deadline remains. | Stop abandoned work and preserve idempotency. | Restart a long operation from zero. |
| 429 / quota | Caller is exceeding a policy. | Honor retry information; shape future traffic. | Expose machine-readable policy when supported. | All clients retry at the same second. |
| 502/503/504 | Transient upstream, overload, or gateway failure is possible. | Small capped retry with jitter; consider stale/read fallback. | Load shed early and return Retry-After when meaningful. | Retry storm during overload. |
| 409 conflict | Current resource state rejects the change. | Fetch/reconcile; do not blind retry. | Return problem details and current revision. | Overwrite concurrent work. |
| 4xx validation/auth | Request or authority must change. | No transport retry. | Stable type, field errors, remediation and request ID. | Spend battery repeating permanent failure. |
Idempotency is a server-side state machine
For a side-effecting operation, the client creates a high-entropy key scoped to principal and route. The server stores key, request fingerprint, state, operation/resource ID, status, response or result reference, timestamps, and expiry. The same key plus same fingerprint returns the original outcome; the same key plus different payload is rejected. Concurrent duplicates wait or receive an explicit “in progress” response. Persist the record atomically with the business effect.
Acknowledge long work asynchronously
If processing exceeds an interactive deadline, accept the command once and return 202 Accepted with an operation URL. The operation exposes pending/running/succeeded/failed/cancelled, progress where meaningful, timestamps, result link, retry guidance, and a stable problem detail. Poll with backoff and ETag, or deliver a signed event. Do not keep a mobile request open for a multi-minute export.
Pagination begins with a stable order
Every list endpoint should be paginated from launch. Cursor tokens should bind the query, filters, sort, snapshot or boundary, direction, and last stable ordering tuple. Prefer immutable unique tie-breakers such as (updated_at, id). Tokens are opaque and integrity-protected. Offset pagination is acceptable for small static admin datasets, but unstable collections cause duplicates and skips under insertion/deletion.
Let clients ask for less
Use bounded page sizes, field masks or sparse fieldsets, summary resources, filters, delta sync, updated_since with stable semantics, and separate media from metadata. Avoid one endpoint that returns a full account graph. A low-bandwidth client should be able to fetch the next actionable screen, not the entire domain model.
Compression is negotiated, measured, and bounded
Support Accept-Encoding and set Vary: Accept-Encoding. Compress text, JSON, protobuf, SVG, and other compressible content above a measured threshold. Avoid recompressing JPEG/AVIF/video and small bodies. Benchmark Brotli, gzip, or Zstandard by size, server CPU, device CPU, battery, and latency. Streaming compression should not buffer the entire response.
Cache and revalidate instead of redownloading
Use explicit Cache-Control, validators such as ETag, If-None-Match, and Last-Modified where semantics permit. Separate user-specific and public content, define Vary correctly, and expose freshness. A degraded UI may show stale data only when product rules allow it, with an as_of timestamp and restricted actions. Cache-Status helps debug cache behavior.
Make large transfer resumable
Upload in chunks with a stable upload ID, offset or part numbers, checksums, expiry, content metadata, and an explicit finalize step. Downloads can use range requests and validators. Retry only missing parts. The tus protocol is one established resumable-upload option. Never mark a file complete before verifying length and digest; preserve the logical attachment ID across reconnects.
Graceful degradation is a product contract
Serve lessSmaller page, summary fields, no thumbnails, reduced precision, lower media quality, deferred enrichment, or cached reference data.
Queue intentSave local command with operation ID, idempotency key, dependency version, expiry, user-visible state, and conflict policy.
Disable risky actionsWhen authorization, price, inventory, balance, or safety state is too stale, allow viewing but block irreversible writes.
Prefer core trafficPrioritize authentication, safety, payment confirmation, sync metadata, and text before analytics, images, and prefetch.
Load shed explicitlyReject optional work early, preserve capacity for critical classes, and publish a retryable problem response.
Show truthful stateOffline, syncing, pending, stale, failed, conflicted, and confirmed are distinct states, not one spinner.
Use a stable machine-readable error contract
RFC 9457 problem details provide type, title, status, detail, and instance. Add stable error codes, field violations, retryability, request/correlation ID, and safe remediation. Do not leak stack traces or make clients parse localized prose. Preserve the specific upstream cause when it is safe and actionable.
Transport improvements do not replace API semantics
HTTP/3 over QUIC can improve behavior under loss and connection changes, while HTTP prioritization can signal urgency. Connection reuse, TLS session resumption, CDN/edge placement, and fewer sequential requests also help. None of these prevents duplicate side effects, unbounded lists, ambiguous errors, or stale writes. Deploy HTTP/3 with fallback and measure by real networks.
Observe attempts, not only requests
Trace one logical operation across all attempts using an operation ID plus per-attempt request IDs. Record DNS/connect/TLS/server/download timing, bytes, compression, cache result, network/carrier, timeout phase, retry reason/delay, idempotency result, page token age, stale mode, queue delay, and final user outcome. Measure retry amplification, duplicate suppression, completion by network class, battery/data cost, and p95/p99.
What I would build
A shared client SDK with deadline propagation, network-aware timeouts, retry classification, capped full-jitter backoff, retry quota, idempotency keys, operation polling, cursor helpers, conditional caching, resumable uploads, local outbox, conflict states, and telemetry. The server platform would provide idempotency storage, problem details, rate/load-shed signals, pagination contracts, field masks, async operations, ETags, request cancellation, and attempt-aware traces.
The principle
A reliable API does not pretend the network is reliable. It makes every uncertain outcome recoverable, every retry bounded, every write deduplicated, every transfer resumable, and every degraded state visible enough for the client and user to make the next correct decision.