Get a quote

Real-Time Order Tracking for Restaurant Delivery: System Design for Lebanon and MENA

Every restaurant operating delivery without real-time order tracking has the same problem: inbound calls spike 30 to 45 minutes after every order because customers do not know whether their food is being prepared, is with the driver, or has gone missing. This is how we design real-time order tracking systems for MENA restaurant and delivery operations, including the architecture decisions that hold up under power cuts and mobile network drops.

Every restaurant operating delivery without real-time order tracking has the same problem: inbound calls spike 30 to 45 minutes after every order because customers do not know whether their food is being prepared, is with the driver, or has gone missing. Each of those calls costs staff time, pulls the driver's attention, and leaves the customer less likely to reorder. The solution is not hiring someone to answer these calls. It is building a system that makes the calls unnecessary. This is how we design real-time order tracking for restaurant and delivery operations in Lebanon and MENA, including the architecture decisions that matter in environments with frequent power cuts and intermittent mobile connectivity.

WebSockets, SSE, or polling: which technology to use?

This architectural choice drives infrastructure cost, connection management complexity, and the behavior of the system when connectivity drops.

Polling is the simplest approach: the client sends a request every N seconds asking for the current order status. It is easy to implement and works over standard HTTP. The cost is proportional to concurrent orders: 100 active orders means 100 requests every 10 seconds, which is 600 HTTP requests per minute just for status checks. Under load, this becomes significant.

Server-Sent Events (SSE) is a one-directional push channel over HTTP. The server sends updates when they happen rather than waiting to be asked. SSE works over HTTP/1.1, is supported by all browsers, and automatically reconnects when the connection drops. It is the right choice for order tracking on the customer side, where data flows in one direction: from the kitchen and driver to the customer.

WebSockets provide a bidirectional persistent connection. They are appropriate for the driver side, where the application needs to both receive instructions from dispatch and send continuous location updates back to the server.

The architecture we use:

  • Customer tracking page: SSE
  • Driver application: WebSocket
  • Restaurant dashboard: SSE or polling at 10-second intervals depending on team preference

Order state machine design

An order is not an entity with static fields. It is an entity that moves through defined stages, and the tracking system's job is to reflect those stages accurately.

The state machine for restaurant delivery:

received
  restaurant accepts:              accepted
  restaurant rejects / timeout:    failed

accepted
  kitchen begins:                  preparing
  cancellation requested:          failed

preparing
  food ready for pickup:           ready

ready
  driver picks up:                 picked_up

picked_up
  delivered to customer:           delivered
  delivery failed:                 failed

Every transition must be recorded with a timestamp in an order_events table, not just by updating the status column in the orders table. The audit trail matters for disputes, for analyzing where orders slow down, and for notifying the customer at the right moments.

In Go, the transition function:

type OrderStatus string

const (
    StatusReceived  OrderStatus = "received"
    StatusAccepted  OrderStatus = "accepted"
    StatusPreparing OrderStatus = "preparing"
    StatusReady     OrderStatus = "ready"
    StatusPickedUp  OrderStatus = "picked_up"
    StatusDelivered OrderStatus = "delivered"
    StatusFailed    OrderStatus = "failed"
)

func (s *OrderService) Transition(ctx context.Context, orderID uuid.UUID, next OrderStatus, reason string) error {
    order, err := s.repo.GetForUpdate(ctx, orderID)
    if err != nil { return err }

    if !s.isTransitionAllowed(order.Status, next) {
        return fmt.Errorf("transition not allowed: %s -> %s", order.Status, next)
    }

    if err := s.repo.UpdateStatus(ctx, orderID, next); err != nil {
        return err
    }
    s.events.Publish(orderID, OrderEvent{Status: next, Reason: reason})
    return nil
}

The events.Publish call is what triggers the SSE push to the customer and the push notification.

Driver location updates: what to store and how often

The driver's current location is a high-frequency data stream. Storing every point in the main database creates unnecessary write pressure and wastes storage on data with a very short useful life.

The current location for each active driver lives in Redis with a short expiry:

func (s *DriverService) UpdateLocation(ctx context.Context, driverID uuid.UUID, loc Location) error {
    key := fmt.Sprintf("driver:%s:location", driverID)
    data, _ := json.Marshal(loc)
    return s.redis.Set(ctx, key, data, 30*time.Second).Err()
}

The 30-second expiry means: if location updates stop arriving (the driver lost connectivity), the key expires automatically and downstream consumers know the location is stale.

Update frequency from the driver app: every 5 to 10 seconds is appropriate. More frequent updates drain the phone battery without meaningfully improving the customer experience. Less frequent updates make the map marker appear to jump.

For the customer's map view, do not push every location update. Only push when the driver has moved more than 20 meters from the last pushed location:

const minDistanceMeters = 20

func shouldPushUpdate(prev, curr Location) bool {
    return haversineDistance(prev, curr) > minDistanceMeters
}

This keeps the map smooth without flooding the SSE channel with updates when the driver is stopped at a traffic light.

Handling connectivity drops in MENA environments

In Lebanon specifically, power cuts and internet outages are part of the operational environment, not exceptional events. A tracking system that fails gracefully when the driver loses signal or the restaurant's power goes out is a fundamentally different design than one that assumes continuous connectivity.

On the driver side, the application must queue location updates locally when offline and flush them when connectivity returns:

type LocationBuffer struct {
    mu      sync.Mutex
    pending []LocationUpdate
}

func (b *LocationBuffer) Add(loc LocationUpdate) {
    b.mu.Lock()
    b.pending = append(b.pending, loc)
    b.mu.Unlock()
}

func (b *LocationBuffer) Flush(ctx context.Context, client *DriverAPI) error {
    b.mu.Lock()
    toSend := b.pending
    b.pending = nil
    b.mu.Unlock()

    if len(toSend) == 0 {
        return nil
    }
    // Send as a batch rather than individual requests
    return client.SendLocationBatch(ctx, toSend)
}

Sending as a batch rather than individual requests reduces the reconnection burst that would otherwise hit the server when the driver comes back online.

On the customer side, the tracking UI should distinguish between these states:

  • Driver moving: recent location updates, map marker animating
  • Driver may be in low coverage: last update was 30 to 90 seconds ago, display the last known position with a "last seen X seconds ago" label
  • Connection lost: no update for more than 2 minutes, show a notice that tracking is temporarily unavailable

A tracking page that shows an outdated position as if it were current is actively misleading. Show the staleness clearly.

Push notifications alongside real-time tracking

Not every customer keeps the tracking page open. Push notifications bring them back at the right moments:

  • Order accepted by the restaurant: confirmation that the kitchen has it
  • Order picked up by the driver: the moment delivery starts
  • Driver 5 minutes away: calculated from current position and routing estimate
  • Order delivered: closes the loop on the customer experience

Four notifications per order is the right number in most cases. Every additional notification reduces the probability that the customer reads the next one.

Do not send marketing push notifications during an active delivery. The customer's attention is on their order. A promotional message at this moment is not just ignored, it is annoying.

Scaling to 100 concurrent orders

One hundred concurrent active orders means 100 SSE connections from customers plus 100 WebSocket connections from drivers plus location updates arriving every 5 to 10 seconds from each driver. This is not a large number, but it exposes architectural mistakes.

A polling-based system where the server queries the database for status on each connection interval would generate roughly 1,200 database queries per minute at this scale. An event-driven system where state changes push updates out to connected clients generates queries only when state actually changes, which is orders of magnitude fewer.

For multi-server deployments, use Redis Pub/Sub to distribute events across server instances:

func (b *EventBus) Publish(orderID uuid.UUID, event OrderEvent) {
    channel := fmt.Sprintf("order:%s:events", orderID)
    data, _ := json.Marshal(event)
    b.redis.Publish(context.Background(), channel, data)
}

func (b *EventBus) Subscribe(orderID uuid.UUID, handler func(OrderEvent)) func() {
    channel := fmt.Sprintf("order:%s:events", orderID)
    pubsub := b.redis.Subscribe(context.Background(), channel)

    go func() {
        for msg := range pubsub.Channel() {
            var event OrderEvent
            json.Unmarshal([]byte(msg.Payload), &event)
            handler(event)
        }
    }()

    return func() { pubsub.Close() }
}

An event published on server A reaches the SSE connections managed by servers B and C without any polling or shared memory.


Key lessons from building delivery tracking in Lebanon

Design for the power cut schedule. In parts of Lebanon, electricity follows a daily schedule that everyone knows. Orders placed 10 minutes before a scheduled cut have a specific risk profile. Build your system to handle a mid-delivery server restart gracefully, not as an exceptional case.

Test under 3G conditions. Restaurant delivery drivers in MENA typically use mobile data. A tracking system that works on a fast connection but degrades badly on 3G loses its value exactly when the driver is most likely to need it, in areas with weak signal.

Monitor open connection counts. Active SSE and WebSocket connections are a real-time operational metric. A spike in open connections that do not close after delivery completion indicates a cleanup bug. Make this visible in your operational dashboard.

Free PDF Download

Enjoying this article?

Enter your email and get a clean, formatted PDF of this article - free, no spam.

Free. No spam. Unsubscribe any time.

Not sure where to start?

Voxire builds delivery management systems and real-time tracking infrastructure for restaurants and logistics operations in Lebanon and the MENA region. If you are building order tracking from scratch or replacing an existing system that does not handle connectivity drops well, we can help.

https://voxire.com/get-a-quote/

Back to blog
Chat on WhatsApp