Explorer le blog
Comment on trace les riders en temps réel pour 0,003 $ par course
2026-04-2314 min de lecture

Comment on trace les riders en temps réel pour 0,003 $ par course

L'architecture complète derrière le tracking temps réel de Dago. Du signal GPS à l'animation fluide sur la carte, le tout pour 40 $/mois sur un seul serveur. 85 % d'économie de bande passante, matching sub-milliseconde, et un chemin clair pour scaler.

architecturetemps-réeloptimisation-coûtsride-hailingdago

Introduction : Construire pour Madagascar

Éditée par l'entreprise Tarondro, Dago est une plateforme de VTC et de livraison en pleine croissance, conçue spécifiquement pour les réalités de Madagascar. Les enjeux y sont majeurs. Les connexions réseau peuvent être instables, les téléphones ont des capacités très variables, et l'économie unitaire dicte la viabilité de l'entreprise. Dans un marché où les marges sont fines, dépenser les tarifs d'un cloud auto-scalant classique pour chaque course n'est tout simplement pas soutenable.

En tant que CTO de Tarondro, mon rôle a été de concevoir un système backend capable de gérer des milliers d'utilisateurs simultanés en temps réel, sans faire exploser les coûts d'infrastructure. Le défi était clair. Il fallait une fiabilité de niveau entreprise, mais avec un budget de startup optimisé.

Le problème à 0,003 $

Chaque plateforme de transport urbain tient ou tombe sur une seule expérience : le client qui regarde un point se déplacer sur une carte.

Ce point n'est pas un effet visuel. Derrière se trouve un pipeline de données temps réel. Ce sont des signaux GPS collectés toutes les quelques secondes depuis le téléphone d'un rider, relayés par internet, traités par un serveur, et poussés sur l'écran du client assez vite pour paraître instantanés.

En faisant les comptes, les résultats étaient sans appel. À 500 courses par jour, l'infrastructure totale revient en moyenne à 0,003 $ par course (calculé sur 15 000 courses mensuelles). Moins d'un centime. Sur un seul serveur à 40 $/mois.

Voici l'architecture qui rend ça possible. Voici surtout les décisions en coulisses.


Pourquoi un VPS mono-serveur à 40 $ plutôt que Firebase + Google Maps ?

Beaucoup de startups sont naturellement attirées par les services managés : en quelques heures, on branche Firebase, Google Maps, et la démo fonctionne.

Le problème, c'est que cette vitesse initiale raconte rarement toute l'histoire. Dès que l'usage réel commence (plus d'utilisateurs, plus d'événements temps réel, plus de contraintes locales), la simplicité de la démo peut se transformer en latence imprévisible et en facture qui grimpe.

Notre décision n'était donc pas idéologique. Elle était économique et opérationnelle.

Notre contrainte principale n'était pas de gagner 2 semaines de dev. C'était de garantir un coût unitaire stable pendant la phase d'adoption.

En une vue :

CritèreVPS mono-serveur (notre choix)Firebase + Google Maps
CoûtFixe au départ (~40-60 $/mois)Variable selon trafic et appels API
PrévisibilitéForte (budget plafonné)Moyenne à faible en croissance
Performance localeOptimisable de bout en boutDépend de plusieurs services externes
DonnéesContrôle complet (infra + logs)Réparties chez plusieurs fournisseurs
DémarragePlus d'ops au débutPlus rapide pour prototyper
Lock-inFaiblePlus élevé

Le vrai arbitrage, c'était simplement :

  1. Aller plus vite en démo, avec une facture qui peut vite devenir imprévisible.
  2. Investir un peu plus en backend, pour garder un coût par course stable.

Pour Dago, en marché émergent avec marges serrées, l'option 2 était plus rationnelle.

En clair : on n'a pas rejeté Firebase ou Google Maps. On les a simplement remis à plus tard, quand leur confort produit apportera plus de valeur que leur coût marginal.

Ce choix nous donne aussi un chemin de montée en charge propre : tant que le coût par course reste sous contrôle, on garde l'architecture sobre ; quand un palier business est atteint, on peut réintroduire des services managés de façon ciblée (traffic ETA, analytics, push), sans réécrire le cœur temps réel.

L'architecture derrière le point qui bouge

L'approche naïve échoue à l'échelle

Le vrai problème, ce n'est pas seulement de "recevoir des coordonnées GPS". C'est de préserver deux choses en même temps : la confiance du client (un point fluide, sans lag) et la marge de l'entreprise (un coût par course soutenable).

L'approche que la plupart des équipes essaient en premier paraît logique : chaque seconde, l'app du rider envoie ses coordonnées au serveur par HTTP, le serveur écrit en base, puis l'app client relit la base chaque seconde.

Ça marche en démo. En production, ça casse vite.

À 100 riders simultanés, c'est déjà 100 écritures/seconde et 100 lectures/seconde, uniquement pour la localisation. À 500 riders, la base croule, le serveur s'étrangle sous les handshakes HTTP, la latence monte, et la facture cloud grimpe plus vite que le chiffre d'affaires.

Autrement dit : l'approche naïve dégrade l'expérience utilisateur et vos marges en même temps. L'approche réfléchie échange un peu de conception contre 85 % de réduction des coûts et de meilleures performances.

La solution : le pipeline à 5 ms

L'architecture repose sur une règle simple : chaque outil fait ce qu'il sait faire de mieux.

💡 En clair : Le polling HTTP, c'est appeler toutes les 3 secondes pour demander « t'es où ? ». WebSocket, c'est garder la ligne ouverte : la position arrive dès qu'elle change.

Quatre briques, quatre rôles distincts :

  • WebSocket maintient une connexion ouverte en permanence entre le rider et le serveur. Pas de handshake HTTP à chaque update : la porte est déjà ouverte, les données circulent librement.
  • Redis garde chaque position en mémoire vive avec un index géospatial natif. Concrètement, chaque GEOADD met à jour la position d'un rider en sub-milliseconde, et chaque GEOSEARCH retrouve les riders les plus proches d'un point en une seule commande. Pas de requête SQL, pas de lecture disque. Redis diffuse aussi chaque mouvement instantanément au client qui suit la course, via les rooms Socket.io.
  • PostgreSQL reçoit les positions en batch toutes les 30 secondes. Il ne voit jamais le débit temps réel. Son rôle : archiver les trajets pour les litiges, l'analytics, et la facturation.
  • OSRM, auto-hébergé sur le même VPS, calcule les itinéraires et les ETAs à partir du réseau routier malgache complet. C'est l'équivalent de Google Maps Directions, mais gratuit et local. Les données restent à Madagascar.

Le résultat : une mise à jour de position traverse le pipeline entier en moins de 5 ms. À 500 riders simultanés, le système absorbe plus de 10 000 opérations par minute, là où une architecture HTTP + PostGIS serait déjà en difficulté à 100.

// Le handler WebSocket complet - un update, quatre résultats
@SubscribeMessage('location:update')
async handleLocationUpdate(client: Socket, dto: LocationUpdateDto) {
  const riderId = client.data.userId;
 
  // 1. Ignorer si ce rider a envoyé un update il y a moins de 2s (trick Redis TTL)
  if (!(await this.redisGeo.checkRateLimit(riderId))) return;
 
  // 2. Mettre à jour la position dans l'index géo Redis (sous 1ms)
  await this.redisGeo.updatePosition(riderId, dto.lat, dto.lng);
 
  // 3. Pousser à tous les clients qui regardent cette course (instantané)
  this.server.to(`ride:${dto.rideId}`).emit('location:update', dto);
 
  // 4. Bufferiser pour l'écriture batch en base (PAS d'insert direct)
  this.batchService.buffer(riderId, dto);
}

Deep Dive Technique

La réduction de 85 % : le tracking deux vitesses

C'est l'insight unique qui a le plus transformé notre modèle de coûts.

Tous les riders n'ont pas besoin d'être trackés à la même fréquence. Un rider à l'arrêt qui attend une course n'a pas besoin d'envoyer du GPS toutes les 3 secondes. Personne ne le regarde. Sa position pour le matching n'a besoin que d'être approximative.

On a conçu deux vitesses de tracking :

État du riderFréquence GPSCoût bande passanteUtilité
Idle (en ligne, en attente)Toutes les 60 secondes~0,25 KB/minPosition approximative pour le matching
En mission (course active)Toutes les 3–10 secondes~7 KB/minTracking temps réel pour le client
Hors ligneRien0 $Pas de tracking

Avec 500 riders en ligne et 50 en mission, on passe de 10 000 messages/min à 1 450 messages/min. Même expérience utilisateur. 85 % de messages en moins.

Quand un rider accepte une course, son app bascule en mode rapide. Quand la course se termine, elle revient au heartbeat lent. Le client ne voit jamais la mécanique.

// Flutter - l'app rider adapte sa fréquence GPS à l'état de la mission
Duration get _interval => switch (_state) {
  RiderState.offline   => Duration.zero,        // pas de tracking
  RiderState.idle      => Duration(seconds: 60), // heartbeat léger
  RiderState.onMission => _getSpeedBasedInterval(), // 3-10s adaptatif
};

Et on est allé plus loin. Même pendant une mission, la fréquence s'adapte à la vitesse du rider :

VitesseIntervallePourquoi
Arrêté (trafic, feu rouge)10 secondesLa position ne change pas
Trafic lent (sous 5 m/s)4 secondesPrécision modérée suffisante
En mouvement rapide (plus de 5 m/s)3 secondesPrécision maximale pour l’animation fluide

Et l'app n'envoie un update que si le rider a bougé d'au moins 10 mètres. Cela empêche le jitter GPS de noyer le serveur de données inutiles.


Trouver le rider le plus proche en moins d'une milliseconde

Quand un client demande une course, on doit trouver le rider disponible le plus proche. Pas en 2 secondes. Pas en 500 millisecondes. En moins d'une milliseconde, parce que la vitesse de dispatch est un avantage compétitif.

Les positions du heartbeat 60 secondes sont déjà dans Redis avec un index géospatial. Une seule commande répond à « donne-moi les 10 riders les plus proches dans un rayon de 5 km » :

// Une commande Redis trouve les 10 riders les plus proches - instantané
const nearby = await redis.call(
  'GEOSEARCH', 'active_riders',
  'FROMLONLAT', customerLng, customerLat,
  'BYRADIUS', 5, 'km', 'ASC', 'COUNT', 10
);

On vérifie ensuite lesquels sont libres et on envoie l'offre au plus proche. S'il refuse, le suivant reçoit l'offre. Le matching entier se fait en mémoire, sans requête base de données.

Pourquoi pas un moteur de matching plus sophistiqué ?

On a étudié le système DISCO d'Uber. C'est l'utilisation de cellules Google S2, de hachage consistant, et du protocole gossip Ringpop pour distribuer des millions de connexions. De l'ingénierie impressionnante pour des millions de chauffeurs.

À l'échelle de Dago (des centaines de riders, pas des millions), Redis GEORADIUS donne le même résultat avec une seule commande au lieu d'un système distribué :

Notre approcheApproche UberQuand on basculerait
Redis GEORADIUSGrille hexagonale S2/H350K+ riders simultanés (besoin de sharding)
Instance Redis uniqueHachage consistant multi-nœudsPlusieurs serveurs Redis nécessaires
Offre séquentielle (plus proche d'abord)Optimisation par lot (pondération ETA)Quand les courses perdues par refus deviennent mesurables
OSRM pour distance routièreGoogle Maps Directions APIQuand l'ETA traffic-aware devient un différenciateur

Le heartbeat 60 secondes donne des positions vieilles de 60 secondes au maximum. Comme un rider met 2 à 5 minutes pour rejoindre le client, l'erreur ajoutée est négligeable. Pas besoin d'un système plus complexe pour l'instant.


Animations fluides sans facture Google Maps

Le client voit un rider qui glisse doucement sur la carte. En réalité, les mises à jour GPS arrivent toutes les 3–10 secondes. Elles n'arrivent pas 60 fois par seconde.

L'astuce : l'interpolation côté client. Quand une nouvelle position arrive, l'app n'y téléporte pas le marqueur. Elle l'anime à 60 images/seconde entre les deux points :

// Flutter - animation entre les pings GPS à 60fps
Timer.periodic(Duration(milliseconds: 16), (timer) {
  final t = (elapsed / duration).clamp(0.0, 1.0);
  final lat = start.latitude + (end.latitude - start.latitude) * t;
  final lng = start.longitude + (end.longitude - start.longitude) * t;
  _updateMarker(LatLng(lat, lng));
});

Pour l'infrastructure cartographique, on a évité toute dépendance fournisseur :

ComposantFournisseurCoût
Tuiles de carteMapbox tier gratuit0 $ (jusqu'à 50K chargements web ou 25K MAUs mobile)
Routage & ETAOSRM auto-hébergé0 $ (réseau routier de Madagascar, tourne sur notre VPS)
GéocodageNominatim0 $ (open-source)

💡 En clair : OSRM, c'est un moteur de calcul d'itinéraires. C'est comme Google Maps Directions, mais qu'on fait tourner nous-mêmes sur notre propre serveur avec l'intégralité du réseau routier malgache. Ça ne coûte rien, et les données restent à Madagascar.

Le seul coût qui pourrait évoluer, c'est Mapbox au-delà de leur généreux tier gratuit (ex. 50 000 chargements web ou 25 000 Utilisateurs Actifs Mensuels sur mobile). Même alors, le coût marginal reste des fractions de centime par course, ce qui est dérisoire par rapport aux revenus qui justifieraient ce volume.


Un serveur qui se protège lui-même

Tourner sur un seul VPS avec 8 Go de RAM signifie qu'on ne peut pas auto-scaler lors des pics. À la place, on a conçu le serveur pour dégrader gracieusement plutôt que crasher.

Budget mémoire : chaque octet compte

ComposantRAM
OS + Docker overhead~800 Mo
PostgreSQL~1,5 Go
Redis (géo + cache)~512 Mo
NestJS App~512 Mo
OSRM (routes Madagascar)~1,5 Go
Nginx~64 Mo

Sur 8 Go de RAM : ~4,9 Go utilisés, ~2 Go de marge pour les connexions WebSocket, ~1,1 Go de buffer de sécurité.

Avec 2 Go de marge et ~20-50 Ko par connexion WebSocket, on tient confortablement 5 000+ connexions simultanées avant d'atteindre une limite.

Quand la mémoire dépasse un seuil, le serveur bascule automatiquement en fréquence plus conservative. Il gagne du temps sans couper de connexions. Quand les connexions approchent du maximum, les nouvelles sont refusées proprement :

// Le serveur se surveille toutes les 10 secondes
@Cron('*/10 * * * * *')
async checkHealth() {
  const heapUsedMB = process.memoryUsage().heapUsed / 1024 / 1024;
  // Sous pression ? Demander aux riders d'envoyer moins souvent
  this.degradedMode = heapUsedMB > 400;
}

C'est le compromis VPS : 40 $/mois fixe au lieu de 200–2 000 $/mois en cloud auto-scalant. Les contraintes forcent de meilleures décisions d'ingénierie.


Livraison fiable pour les messages qui ne peuvent pas être perdus

Les mises à jour de position peuvent être perdues. La suivante remplace la précédente. Mais certains messages ne doivent pas être perdus : une offre de course, une annulation, une confirmation de paiement.

Pour ceux-là, on a implémenté un pattern de livraison au-moins-une-fois inspiré du système RAMEN d'Uber. Le serveur envoie, attend l'accusé de réception, et retente si rien n'arrive dans les 5 secondes :

// Le serveur stocke le message en attente, retente si pas d'ACK
await this.redis.setex(`pending:${riderId}:${seq}`, 30, payload);
server.to(`rider:${riderId}`).emit('ride:offer', message);
// Après 5s sans ACK → retry. Après 2 retries → rider suivant.
Type de messageLivraison fiable ?Pourquoi
ride:offer✅ OuiOffre perdue = chiffre d'affaires perdu
ride:cancelled✅ OuiLe rider doit le savoir immédiatement
location:update❌ NonLe prochain update la remplace
eta:update❌ NonLes ETA périmées se remplacent vite

C'est crucial à Madagascar où les réseaux Telma et Orange sont parfois instables. Un rider qui rate une offre à cause d'une micro-coupure de 3 secondes, c'est du revenu perdu. La couche de retry ne coûte presque rien et l'empêche.


Le business case en un tableau

StadeCourses/jourRiders simultanésCoût mensuelCoût par course
MVP1002040–60 $0,003 $
Croissance1 00020080–120 $0,005 $
Scale5 0001 000150–200 $0,004 $

Le coût par course reste sous un centime pendant les premiers milliers d'utilisateurs actifs quotidiens. Comparez avec une approche cloud-first naïve : 500–2 000 $/mois au stade Croissance pour les mêmes charges.

La différence n'est pas la complexité technique. C'est l'intentionnalité de conception.


Chemin de scaling : pas de réécriture, juste de la configuration

L'architecture a un chemin de montée en charge clair à chaque point d'inflexion :

Phase 1 :  VPS unique. Tout sur un serveur (40-60 $/mois)
               │  Signal : 5K+ connexions WebSocket soutenues
               ▼
Phase 2 :  Services séparés. PostgreSQL + OSRM sur 2ᵉ VPS (80-120 $/mois)
               │  Signal : 85%+ d'utilisation RAM soutenue
               ▼
Phase 3 :  Multi-serveur. Ajout adaptateur Redis (1 ligne de code) (150-200 $/mois)
               │  Signal : 50K+ connexions simultanées
               ▼
Phase 4 :  Migration cloud. Kubernetes, infra managée

La transition Phase 1→2 est un changement de configuration Docker. Phase 2→3 est littéralement un import :

// Une ligne pour passer de mono-serveur à multi-serveurs
import { createAdapter } from '@socket.io/redis-adapter';
io.adapter(createAdapter(pubClient, subClient));

Pas de réécriture. Pas de migration. Pas de changement d'architecture.


Ce qu'on n'a pas construit (et ce qui a raté)

Un document d'architecture honnête inclut les lacunes.

Le tracking « à la demande » : une fausse bonne idée

Notre première architecture était un piège élégant. L'idée de départ semblait brillante : ne tracker aucun rider en permanence. Quand un client demande une course, le serveur envoie une notification silencieuse à tous les riders proches. Leur téléphone se réveille, récupère sa position GPS, et la renvoie au serveur. Le serveur compare les réponses et attribue la course au plus proche. Zéro connexion WebSocket. Zéro coût quand personne ne commande. Sur le papier, c'est du génie.

En pratique, ça s'effondre pour trois raisons que seule la production révèle :

  1. La latence des notifications push est imprévisible. Une notification silencieuse via FCM (Firebase Cloud Messaging) met entre 500 ms et 15 secondes à arriver, selon le réseau, l'OS, et si le téléphone est en mode Doze. Le client qui attend un rider voit un spinner pendant 5 à 15 secondes avant même que le matching commence. Sur les réseaux Telma et Orange à Antananarivo, c'était souvent plus proche des 10 secondes. Inacceptable.

  2. L'OS tue l'app en arrière-plan. Android et iOS tuent agressivement les apps en arrière-plan pour économiser la batterie. Une notification silencieuse suppose que l'app est encore vivante pour la recevoir et exécuter du code. Sur les téléphones d'entrée de gamme (la majorité de nos riders), l'app était tuée en quelques minutes. Le rider semblait « en ligne » côté serveur, mais son téléphone ne recevait plus rien.

  3. Le GPS cold start ajoute 5 à 30 secondes. Quand le téléphone n'a pas utilisé le GPS récemment, le premier fix prend du temps. Notification (5s) + GPS cold start (10s) + réponse serveur (1s) = le client attend potentiellement 15 secondes juste pour le matching, sans compter le temps de trajet du rider.

Le tracking continu par WebSocket avec le heartbeat 60 secondes coûte un peu de bande passante, mais il élimine les trois problèmes d'un coup. Le rider est déjà connecté, sa position est déjà dans Redis, et le matching se fait en sub-milliseconde. La leçon : une architecture « zéro coût au repos » ne vaut rien si elle détruit l'expérience au moment critique.

Les compromis assumés (pour l'instant)

On a sauté le surge pricing. Uber utilise des grilles hexagonales H3 pour découper les villes en cellules de demande et ajuster les prix dynamiquement. On y a pensé, mais le volume ne le justifie pas encore. Quand ce sera le cas, le package h3-js permet de mapper les coordonnées en cellules hexagonales côté Node.js, qu'on peut ensuite stocker dans des clés Redis standards. C'est une migration propre depuis notre GEOSEARCH actuel.

On a sauté l'ETA traffic-aware. Notre routage OSRM donne des ETAs précises en distance routière, mais ne tient pas compte du trafic en temps réel. Pour les embouteillages d'Antananarivo, nos ETAs peuvent être décalées de 2–5 minutes aux heures de pointe. L'API Google Maps Traffic corrigerait ça. Cela coûte 7 $ pour 1 000 requêtes. On l'ajoutera quand le volume de courses le justifie.

On a choisi JSON plutôt que Protobuf. C'est correct pour l'instant. À notre échelle, les payloads JSON coûtent environ 5x plus de bande passante que la sérialisation binaire. À 500 riders, c'est la différence entre 110 Go et 22 Go par mois. Les deux sont largement dans les limites du VPS. À 10 000 riders, Protobuf deviendra une optimisation sérieuse.

On a sous-estimé le jitter GPS. Les premiers tests à Antananarivo ont montré une précision GPS qui rebondissait de 20–50 mètres en zone urbaine dense. Le seuil de mouvement de 10 mètres attrape la plupart du jitter, mais les riders à l'arrêt « dérivent » parfois sur la carte du client. Un filtre de Kalman lisserait ça. On ne l'a pas encore implémenté.

Pas de stack de monitoring. On a délibérément sauté Grafana/Prometheus pour économiser ~500 Mo de RAM sur le VPS. On se repose sur docker stats et les logs applicatifs. Ça marche, mais on a eu deux incidents où on n'a découvert la pression mémoire qu'après des signalements utilisateurs. Une solution de monitoring légère est la prochaine priorité.


Ce qu'on a appris

La bonne architecture ne consiste pas à copier l'infrastructure d'Uber ou à empiler les services Cloud à la mode. Elle consiste à faire correspondre la technologie aux contraintes de votre business.

Pour Dago, cela signifiait accepter que les réseaux mobiles de Madagascar ne sont pas parfaitement stables, et qu'un coût unitaire d'un demi-centime était la condition de survie du projet. Le tracking « deux vitesses », le routage auto-hébergé et le pipeline WebSocket + Redis ne sont que la traduction technique de ces contraintes.

La leçon transportable pour n'importe quel produit : chaque système a une optimisation « deux vitesses » cachée sous ses yeux. Qu'il s'agisse de capteurs IoT, de pipelines d'analytics ou de systèmes de notifications, la plupart des entreprises paient cher pour traiter à haute fréquence des données qui n'ont pas toutes la même valeur.

Identifiez ce qui ne nécessite pas une résolution à la seconde. Ralentissez-le. Sortez votre base de données de la boucle temps réel. Et regardez vos coûts chuter de 85 %, sans jamais dégrader l'expérience de vos utilisateurs.


Si vous construisez une plateforme avec des contraintes similaires (fonctionnalités temps réel, infrastructure économe, marchés émergents), parlons de votre architecture. La première session de 30 minutes est gratuite, sans engagement.