Building Offline-First Sync Engines for 2G Networks: Lessons from Rural Healthcare
When Mathéo, a second-year CS student at EPITECH Nancy, set out to build a sync engine for rural clinics, he faced a problem most developers never encounter: designing for networks so slow and unreliable that modern assumptions about connectivity completely break down.
His solution? Simulate 2G network conditions locally and build a Rust-based offline-first sync engine that treats connectivity as a luxury, not a requirement. The project offers a masterclass in constraint-driven architecture that every backend engineer should study.
Why 2G Simulation Matters for Modern Systems
Rural healthcare facilities in developing regions often operate on 2G networks—or worse. We're talking about:
- 25-50 kbps bandwidth (comparable to dial-up)
- 500-1000ms latency on good days
- Frequent disconnections lasting hours or days
- Unpredictable packet loss during weather events
Most developers build applications on fiber connections and test on WiFi. Even "slow 3G" throttling in Chrome DevTools (400ms RTT, 400 kbps) is 8-16x faster than real 2G conditions.
Mathéo's approach was to use network simulation tools (likely tc on Linux or similar traffic control utilities) to artificially degrade his local development environment. By forcing his sync engine to operate under authentic 2G constraints from day one, he avoided the classic trap of building an "offline mode" that only works on temporarily disconnected 4G.
The Offline-First Sync Architecture
Building for 2G isn't just about compression and retry logic—it requires rethinking your entire data model:
Local-First Data Storage
The sync engine operates on a simple principle: the local database is the source of truth. When a nurse records patient vitals or updates medication records, that write:
- Commits immediately to the local SQLite or embedded database
- Returns success to the user instantly (no network round-trip)
- Queues the change for eventual sync in the background
This inverts the traditional client-server model. There's no "saving to the cloud"—there's only local persistence and opportunistic synchronization.
Conflict-Free Replication
When multiple clinics operate independently for days, then sync, conflicts are inevitable. Mathéo's Rust implementation likely uses CRDTs (Conflict-Free Replicated Data Types) or a similar approach:
- Last-write-wins for simple fields with timestamps
- Merge semantics for append-only logs (lab results, visit history)
- Causal ordering to preserve "happens-before" relationships
Rust's type system shines here. Encoding sync state as an enum (Synced, Pending, Conflict) and using pattern matching ensures the code handles every state transition explicitly.
Bandwidth-Conscious Delta Sync
On a 2G connection, sending full documents is prohibitively expensive. Instead, the engine likely:
- Tracks changes at field granularity, not document-level
- Compresses deltas using binary formats (MessagePack, CBOR)
- Batches updates to amortize connection overhead
- Prioritizes critical data (patient allergies before billing records)
A 500 KB JSON patient record becomes a 2 KB binary delta representing "blood pressure updated, last sync timestamp changed."
Why Rust for Sync Engines?
Mathéo chose Rust, and for sync infrastructure operating at the edge, it's an inspired choice:
Memory Safety Without Garbage Collection
Rural clinics often run on older hardware—refurbished laptops, ARM single-board computers. A GC pause that stalls a Java or Node.js process for 100ms isn't noticeable on a developer's MacBook Pro. On a clinic's 4 GB Raspberry Pi managing a local database and sync queue? That pause can corrupt in-progress writes.
Rust's ownership model guarantees memory safety without GC, giving predictable latency even on constrained hardware.
Fearless Concurrency
A sync engine juggles:
- User writes to the local DB
- Background sync worker polling the queue
- Network reconnection detection
- Conflict resolution logic
Rust's borrow checker makes it impossible to accidentally share mutable state across threads. You can't ship a data race to a clinic where debugging requires a multi-hour drive.
Binary Size and Startup Time
A statically-linked Rust binary can be 5-10 MB. The equivalent Electron app? 100+ MB. When your distribution method is "copy to USB drive and physically mail it," binary size matters.
Key Takeaways for Offline-First Design
Whether you're building for rural clinics or just want your SaaS to survive flaky conference WiFi, Mathéo's approach teaches crucial lessons:
-
Simulate constraints early: Don't test offline mode the week before launch. Throttle your dev environment from day one.
-
Design for partitions, not outages: "Offline mode" implies temporary disconnection. "Offline-first" means partitioned operation is the default state.
-
Local writes never fail: The user's action (save, submit, record) must succeed immediately. Sync is an implementation detail.
-
Conflicts are data, not errors: Don't treat sync conflicts as exceptional. Model them explicitly in your types and UI.
-
Test on real hardware: Your MacBook Pro has 10x the RAM and 100x the SSD speed of deployment targets. If you only test there, you're guessing.
The full project details are available on Dev.to (search for "Simulating 2G to build an offline-first sync engine in Rust"), and it's worth reading Mathéo's implementation notes. It's rare to see a student project tackle a real-world infrastructure problem with this level of rigor.
The next time you write if (navigator.onLine), ask yourself: would this code work on a 2G connection in rural France? If not, you're not building offline-first—you're building online-required with a wishful fallback.