A courier tracking project often starts with a simple task: showing the client where the delivery is. But production requires solving dead-reckoning upon GPS loss, map snapping to the road graph, and synchronization between three components: the courier app, the server bus, and the client app. On a project with 500 couriers, we reduced update latency to 2 seconds by switching from WebSocket to MQTT and configuring Redis with a TTL of 60 seconds. This approach saves development hours and lowers operational costs by up to 30% due to reduced traffic. Mistakes in protocol selection or battery mode configuration lead to code rewrites — so it's crucial to lay the right architecture from the start. Order an audit of your current solution — we'll choose the optimal configuration.
Why Courier Tracking Is Not Just GPS?
A production system includes four key components:
- Dead-reckoning upon GPS signal loss — interpolation of last known coordinates using speed and heading.
- Map snapping to roads — raw GPS points jump relative to the road; they need to be snapped to the road graph.
- Courier state logic: idle → assigned → picking_up → delivering → completed.
- Battery trade-off on the courier app side: 5 seconds versus 15 seconds.
Courier App: Collecting and Transmitting Coordinates
On Android we use FusedLocationProviderClient from play-services-location in a Foreground Service. The update interval is a compromise: 5 seconds gives accuracy, 15 seconds saves battery. For pedestrian couriers — Priority.PRIORITY_HIGH_ACCURACY + 5 seconds. For automotive — Priority.PRIORITY_BALANCED_POWER_ACCURACY + 10 seconds with setMinUpdateDistanceMeters(20f).
On iOS — CLLocationManager with desiredAccuracy: kCLLocationAccuracyBestForNavigation in active mode and switching to Significant Location Changes in background. activityType = .automotiveNavigation activates GPS noise filtering.
Anomalous point filtering is mandatory. GPS in city streets produces jumps of 50–200 meters. A simple filter: discard a point if horizontalAccuracy > 50 meters or calculated speed > 200 km/h.
func shouldAcceptLocation(_ location: CLLocation) -> Bool { guard location.horizontalAccuracy > 0, location.horizontalAccuracy <= 50 else { return false } if let lastLocation = lastAcceptedLocation { let timeDelta = location.timestamp.timeIntervalSince(lastLocation.timestamp) let distance = location.distance(from: lastLocation) let impliedSpeed = distance / timeDelta if impliedSpeed > 55.6 { return false } } return true } Buffering in a local database plus batch sending upon network restore is a standard scheme.
Server Bus: WebSocket or MQTT?
| Feature | WebSocket (Socket.IO) | MQTT (EMQ X) |
|---|---|---|
| Traffic | Higher due to headers | Lower by 3x thanks to binary frames |
| Mobile network behavior | Worse — connection breaks require reconnection | Better — QoS + persistent sessions |
| Scalability | Suitable up to a few thousand couriers | Easily scales to tens of thousands |
| Stack | Node.js, FastAPI | Mosquitto, EMQ X, AWS IoT Core |
For small loads we choose Socket.IO. For scale — MQTT. MQTT defines a binary protocol, achieving up to 70% traffic savings on mobile networks.
Storage of current position — Redis: SET courier:{id}:position with TTL 60 seconds. Route history — TimescaleDB.
Map snapping. Raw GPS coordinates are aligned to roads via Google Roads API or OSRM self-hosted (free, <10 ms).
How to Ensure Smooth Marker Animation?
Updates every 5–10 seconds. Without animation, the marker jumps. We interpolate movement between two points.
On Android via ValueAnimator:
val animator = ValueAnimator.ofFloat(0f, 1f).apply { duration = 3000 interpolator = LinearInterpolator() addUpdateListener { animation -> val fraction = animation.animatedFraction val lat = prevLat + (newLat - prevLat) * fraction val lon = prevLon + (newLon - prevLon) * fraction marker.position = LatLng(lat, lon) } } animator.start() On iOS — CADisplayLink or UIView.animate with custom timing. Marker rotation angle is calculated via atan2.
In Flutter — TweenAnimationBuilder with Tween<LatLng>. For google_maps_flutter we update Marker(position: interpolatedPosition) in a Ticker every 16 ms.
Courier States
| State | Description | Client UI |
|---|---|---|
| idle | Courier free | "Looking for a courier" |
| assigned | Assigned to order | "Courier is heading to restaurant" |
| picking_up | Picking up order | "Courier at restaurant" |
| delivering | Delivering order | "Courier is delivering your order" |
| completed | Delivered | "Order delivered" |
Transitions are managed by the server. Order development for your scenario — we'll account for all nuances.
What's Included
- Protocol design (WebSocket/MQTT) and data schema.
- Courier app development with background tracking.
- Server bus implementation with Redis and TimescaleDB.
- Client UI development with smooth animation and states.
- Load testing: 100/500/1000 couriers.
- Documentation and a training session.
Process and Timeline
- Analysis: audit of current infrastructure, protocol selection.
- Design: bus architecture, state schemas.
- Development: app and server implementation.
- Testing: unit, integration, load.
- Deployment: release to App Store and Google Play.
Timeline: from 2 to 4 weeks depending on the number of platforms and server API readiness.
Why Work With Us?
We have implemented tracking for 15+ logistics projects. Our engineers are certified in iOS and Android. We guarantee track accuracy up to 5 meters and stability with 10,000 concurrent sessions. Get a consultation for your case — contact us.







