Engineering

Offline-first field apps: what breaks when the signal goes

“It needs to work offline” is usually scoped as a storage problem, and storage is the part a library already solves. What breaks sits at the boundary where the queue meets the server.

Anitha Prince Founder · · 6 min read

In short

  • Ofcom’s January 2026 figures put good 4G across 84% of the UK landmass from all operators and 96% from at least one, so a crew on a single network loses usable signal across roughly a sixth of the country.
  • Server-side idempotency keys have to be retained for longer than the crew’s worst offline stretch. Stripe’s documented 24-hour pruning window is an API default, and a phone that returns to coverage on Monday morning has been gone for more than 60 hours.
  • A CRDT guarantees that every device converges on the same value, not that the value is correct. Automerge keeps the losing writes in a conflicts object that most applications never read.
  • Android 15 allows all of an app’s dataSync foreground services 6 hours in total per 24-hour period, so sync lands later than designed and everything downstream has to tolerate late data.

Every field software brief has the same line in it: it needs to work offline. It is almost always scoped as a storage problem — put a database on the phone, queue the writes, send them when the signal returns. That part is solved. SQLite ships on every device and several credible sync engines sit on top of it.

What breaks is the boundary where the queue meets the server, and none of it shows up in testing, because testing happens in an office where the only failure mode anyone tries is aeroplane mode.

The local database is the easy half

Start with how much of the day is affected. Ofcom’s January 2026 figures put predicted 4G across 96% of the UK landmass from at least one operator, but good 4G from all operators across only 84%. A crew is on one network, so roughly a sixth of the country is a partial not-spot for whoever you handed the phones to — before counting basements, plant rooms, steel frames and lift shafts, which do not appear in a landmass figure at all.

For construction and field services, a delivery round or a vessel working a ground with no coverage at all, offline is not an edge case. It is the normal condition for part of every day, and treating it as an exception is how you get an app that is correct on Wi-Fi and lossy everywhere else.

One bar is worse than no bars

Aeroplane mode is the test everyone runs, and it is the kindest case: the request fails instantly and the app knows where it stands. The state that costs you data is one bar. A request over a degraded link does not fail, it hangs — on iOS the default request timeout is 60 seconds without new data arriving. The worker taps Submit, watches a spinner for a minute, and taps again.

So do not ask the operating system whether you are online. Connectivity APIs report the state of the radio, not whether your server answered, and will report a healthy connection while every request dies. Decide by outcome: a write is unacknowledged until the server says otherwise. Then make the queue visible — a count of unsent items on the main screen, and a list the worker can open. If the person holding the phone cannot tell whether their day’s work has left the device, the app loses data quietly, and you learn about it a fortnight later from an invoice that does not match.

The idempotency window has to outlast the outage

A request that hangs and later succeeds becomes a duplicate unless the server can recognise it. The standard answer is right: generate an idempotency key on the device for every write and store the result against it server-side, so a repeat returns the original outcome instead of a second record.

The detail that gets missed is how long the server remembers. Stripe’s documented behaviour is that keys can be removed once they are at least 24 hours old, and that a key reused after pruning generates a new request. For a payments API that is generous, because the retry happens seconds later. For a field app it is nowhere near enough: a survey completed in a valley on Friday afternoon and synced when the van reaches the depot on Monday is more than 60 hours old, and a vessel is out for a week. Set retention from the crew, not from the library default — thirty days of stored keys costs a table and an index, and unpicking duplicate job records by hand costs someone a morning a month.

Conflicts are a business decision, not a merge algorithm

This is where sync engines are sold, and where the pitch and the engineering part company. CRDTs do merge automatically and they genuinely converge — but look at what convergence means. Automerge’s documentation is straightforward: when two devices write the same property, it “picks one of the concurrently written values as the ‘winner’, and it ensures that this winner is the same on all nodes”. The rest are “relegated to a conflicts object” almost no application ever reads.

A sync engine guarantees that every device ends up with the same answer. Nothing in it guarantees the answer is the right one.

The work worth doing before choosing any library is to classify every field into one of three kinds:

  • Append-only facts — a meter reading, a photo, a signature, a temperature at landing, a defect raised. These cannot conflict: two engineers recording the same crack produce two records, and both are true. Most of a field app is this.
  • Server-owned state — job status, stock allocation, whether a unit has been let. The client proposes; the server decides and is allowed to refuse. Last-write-wins here is how a job gets marked complete by whichever engineer happened to leave site first.
  • Genuinely co-edited content — free text two people work on at once. This is the only class where a CRDT earns its complexity, and in field software it is usually one notes field.

Sort the schema that way and most of the merge problem disappears, because only the third class needs a merge at all.

One thing not to build any of it on: the device clock, which drifts and can be set by hand. Record two times on every event — what the device says the work happened at, which is the audit record because it is what the worker saw, and when the server received it, which is the only ordering authority you control.

The operating system decides when your sync runs

You do not control when the queue drains; platform rules do, and they have tightened. Android 15 permits an app’s dataSync foreground services “to run for a total of 6 hours in a 24-hour period”, shared across every such service the app runs, after which the system calls the service’s timeout method and throws if it does not stop itself within seconds.

So data arrives hours after the event, and that is normal rather than a fault. Everything downstream has to tolerate it: a report that runs at midnight and treats whatever is in the database as the full day is wrong on any day a van was in a valley.

Ordering is the other trap. PowerSync documents its upload queue as “a blocking FIFO queue” that “cannot advance if the backend does not acknowledge a mutation”. That is right for consistency, and it means one write the server keeps rejecting — a validation rule tightened last week, a field that became mandatory — stops a whole crew’s day uploading behind it. Every queue needs a quarantine: after a set number of attempts, move the poisoned item aside, alert a named person, let the rest through.

Choose a sync engine you can survive losing

Field software has a long life — a job app rolled out to forty engineers is still running in five years, because retraining a field workforce is the expensive part — and sync engines are a young category. The Realm repository records what became of the obvious default: the deprecation of Atlas Device Sync and the Realm SDKs was announced in September 2024, and the service reached end of life on 30 September 2025. Realm is still a good local database. It was the sync, the part hardest to replace, that went.

So keep the boundary. The rest of your mobile app should say “record this inspection” and “give me today’s jobs”, and one adapter should know whether that means PowerSync, WatermelonDB or endpoints you wrote yourself. It is nearly free on day one and expensive to retrofit, and it gets skipped for the same reason every time: on the first day, the vendor looks permanent.

The four tests worth running before sign-off

Run the app the way it will be used, not the way it demos. Capture a full day’s work with the radio degraded rather than switched off. Reboot the device with items still in the queue. Have the server reject one write and confirm the rest still upload. Set the clock five minutes wrong and check nothing depends on it. Those four cases account for most of what goes missing in the first month of a rollout, and all four can be run on a desk in an afternoon.

Common questions

What does offline-first actually mean for a mobile app?

It means the device is the primary place work is recorded, and the server is somewhere that work is later reconciled to. The app writes to a local database immediately and never blocks a worker on a network call. The engineering effort goes into the queue, the conflict rules and the server’s tolerance of late data, not into the local storage itself.

How do you handle sync conflicts in an offline-first app?

Classify every field first. Append-only facts such as readings, photos and signatures cannot conflict, because two records are simply two records. Server-owned state such as job status or stock allocation should be proposed by the client and decided by the server. Only genuinely co-edited text needs a merge algorithm, and in field software that is usually one notes field.

Why do offline-first apps create duplicate records?

Because a request over a weak connection hangs rather than fails, so the app cannot tell whether the write landed. The fix is a client-generated idempotency key on every write, stored server-side against the result, with a retention period longer than the longest realistic time a device spends out of coverage.

Can you trust the timestamp on a phone in the field?

No. Device clocks drift and can be set by hand, so they should never decide which of two writes wins. Record what the device says the work happened at as part of the audit trail, and use a server-assigned received time for ordering.


Written by the Voxalone team. If this raised a question about your own systems, get in touch — you will get a straight answer from an engineer, with no obligation attached.

Keep reading

More insights

Engineering

Integrating with systems that have no API

“No API” rarely means no way in. It means no documented one. What matters is how far from the screen you can get, and whether you are reading from the system or writing to it.

Read more
Engineering

Replacing the spreadsheet only one person understands

The formulas are the easy part. What you are actually replacing is a specification that exists in one person’s head, and the projects that fail are the ones that never went looking for it.

Read more

Next step

Let’s work out whether we can help

Tell us what you are trying to achieve. You will speak to an engineer, not a salesperson, and you will get a straight answer about whether this is something we should take on.

We reply to every enquiry within one working day.