Most apps are designed and tested on fast office Wi-Fi. Real users are elsewhere: on mobile data in a car, in a basement, in a building where the router just lost power, in a village with one bar of signal. In many places, connections that drop or slow to a crawl are a normal part of the day, not an edge case.
An app that assumes a perfect network fails in frustrating ways: spinners that never end, forms that lose what you typed, actions that might or might not have happened. An app designed for unreliable connections stays useful and, just as important, stays honest about what has been saved.
This matters most for field apps. The technician app I built for NeonTask is used for jobs, check-ins, notes and photos on site, where connectivity is whatever the building provides. These are the patterns I use.
Decide what must work offline
Full offline support for everything is expensive. Decide per feature:
- Must work offline: viewing today's jobs or bookings, reading details already loaded, writing notes, taking photos, marking steps done.
- Should degrade gracefully: searching, loading older history, viewing maps.
- Can require a connection: payments, account changes, anything that must be confirmed by the server instantly.
This list drives the architecture. Write it down with the product owner before building.
Cache what users need to read
The app should show the last known data immediately, then refresh in the background. In React Native, libraries like TanStack Query with a persisted cache, or a local database such as SQLite, make this straightforward.
For a field app, prefetch what the user will need: when the technician opens the app in the morning on good Wi-Fi, download today's jobs with their addresses, notes and any reference files. Show when the data was last updated, so users know if they are looking at something stale.
Queue what users write
The key pattern for writes is an outbox: every action the user takes (a note, a status change, a photo) is saved locally first and added to a queue. A background process sends queued items to the server when the connection allows, in order, and marks them done when the server confirms.
From the user's point of view, the action happened immediately. The UI shows it as "saved on device, syncing" until the server confirms, then as synced.
Without this, every write is a gamble: the user taps "Complete job", the request hangs, they tap again, and now you have either nothing or duplicates.
Make retries safe with idempotency
Networks fail in the worst place: after the server has processed a request but before the response reaches the phone. The app thinks it failed and retries, and the server processes it twice.
The fix is idempotency. The app generates a unique ID for each action when it is created, and sends it with the request. The server stores processed IDs and, if the same ID arrives again, returns the original result instead of repeating the action. This makes retries safe, and it is essential for anything involving money, bookings or status changes.
Pair it with sensible retry logic: exponential backoff, a cap, and retrying when the device reports connectivity has returned.
Handle photos and large uploads separately
Photos are where field apps struggle most. A few high-resolution images can be several megabytes each, and a weak connection cannot push them through in one request.
- Compress and resize on the device before upload. Most job photos do not need full camera resolution.
- Upload photos in their own queue, separate from small data updates, so a stuck photo does not block a status change.
- Upload directly to object storage with a short-lived upload URL where possible, so large files do not tie up the API.
- Resume or retry individual files, not the whole batch.
- Keep the local file until the server confirms it has it.
Resolve conflicts deliberately
If two people can edit the same record, offline edits will eventually conflict. Decide the rule per type of data:
- Append-only data (notes, photos, check-ins) rarely conflicts. Each is a new record. Design data this way where you can.
- Status changes often follow a workflow. The server validates whether the transition is still allowed and rejects it clearly if not, for example if the job was cancelled meanwhile.
- Editable fields can use "last write wins" with timestamps when the stakes are low, or flag the conflict to a human when they are not.
Tell the user when a queued action was rejected, and why. Silently dropping it is the worst outcome.
Design honest UI states
Users trust an app that tells them the truth:
- Show a clear but unobtrusive offline indicator.
- Mark items that are saved locally but not yet synced.
- Never show a spinner without a timeout. After a reasonable wait, show a message and keep the user's input.
- Never clear a form until the data is safely stored, at least locally.
- Provide a "sync now" action and a view of pending items for power users and support.
Web apps too
The same thinking applies to web apps used in the field or on weak connections. Keep page weights small, cache static assets aggressively, save form drafts in local storage, and use the same idempotent request pattern. Service workers can provide real offline support for web apps when needed, though for heavy offline use a native or React Native app is usually the better tool.
The server side
Offline tolerance is not only an app concern. The backend should:
- Accept idempotency keys and store results.
- Accept client timestamps for when an action happened, separately from when it arrived.
- Validate state transitions and return clear, machine-readable errors.
- Support fetching changes since a given time, so apps can sync efficiently.
The API for NeonTask, for example, is a .NET Core service that handles work orders, technicians and their updates. Designing it with sync in mind from the start is much cheaper than retrofitting it. See API design for a mobile backend.
Test with a bad network on purpose
Use the network throttling tools in iOS and Android developer settings, or a proxy, to simulate slow and lossy connections. Test these scenarios explicitly:
- Turn off the network mid-submit.
- Kill the app with items still queued, reopen it.
- Submit the same action from two devices.
- Upload ten photos on a slow connection.
If the app survives these, it will survive a normal day in the field.
Is it worth it?
For an app used indoors on stable Wi-Fi, basic caching and good error messages may be enough. For apps used on the move or on site, offline tolerance is the difference between an app staff rely on and one they work around. Plan it from the start; it shapes the data model and the API.
If you are building an app for field staff or for customers on the move, see my work and get in touch. The trade-offs between platforms are covered in React Native vs native.