Real-Time Order Tracking with AWS AppSync: How We Cut Update Latency by 94%

How we replaced REST polling with AWS AppSync subscriptions at Rafeeq: live order tracking and partner alerts in under a second instead of 45.

#aws#appsync#real-time#graphql#websockets
Cover image for the article: Real-Time Order Tracking with AWS AppSync: How We Cut Update Latency by 94%

At Rafeeq we process over 16 million orders a year, and each order goes through roughly 12 status transitions: placed, accepted, preparing, picked up, on route, delivered. That's about 190 million status events a year, and every single one of them matters to three people at once: the customer watching the map, the partner preparing the order, and the driver on the road.

For years we did what most apps do: the mobile clients polled a REST endpoint every 10 seconds. It worked, and it quietly hurt us everywhere.

The problem with polling

  • It's slow when it matters. With a 10-second interval plus network and processing time, the median delay between a real status change and the user seeing it was 7.5 seconds, and p95 was 14.2 seconds. A customer watching a "preparing" screen while their food is already on the road is a support ticket waiting to happen.
  • It's wasteful. At peak we handled ~6,000 concurrent tracking sessions per city. Polling generated 182 million status-check requests per day, and our logs showed 94% of them returned no change at all.
  • Partners reacted late. New-order alerts reached partner tablets on the next poll cycle. Average time from order placement to partner acceptance was 45 seconds. In quick-commerce, that delay compounds into late deliveries.

Why AppSync

We evaluated three options: raw WebSockets on our own infrastructure (API Gateway + Lambda), Firebase Cloud Messaging as a signal channel, and AWS AppSync. AppSync won for three reasons:

  1. Managed WebSocket fan-out. One GraphQL mutation fans out to every subscribed client. No connection registry, no heartbeat handling, no scaling WebSocket servers ourselves. AppSync handles hundreds of thousands of concurrent connections natively.
  2. Subscription filtering. A driver subscribes to their orders, a partner to their store, a customer to their single order. Server-side filtering means clients only receive what they're authorized to see, enforced with Cognito claims.
  3. It fit our stack. Resolvers connect directly to DynamoDB and Lambda, and IAM handles service-to-service auth. We were fully on AWS already; there was no new operational surface to own.

The architecture

Architecture diagram: order service publishes a mutation to AWS AppSync, which pushes updates over WebSockets to the customer app, partner app, driver app, and ops dashboard in under one second

The flow is deliberately boring:

  1. The order service writes a status change and calls a GraphQL mutation on AppSync (server-to-server, IAM-signed).
  2. AppSync evaluates subscription filters: order ID for customers, store ID for partners, driver ID for drivers.
  3. Every matching client gets the update pushed over the existing WebSocket: no new request, no polling loop.
subscription OnOrderUpdate($storeId: ID!) {
  onOrderUpdate(storeId: $storeId) {
    orderId
    status
    etaMinutes
    courierLocation { lat lng }
  }
}

The results

Latency was the headline win. Median propagation dropped from 7.5 seconds to under half a second:

Bar chart comparing order-status propagation latency: REST polling before at 7.5 seconds median and 14.2 seconds p95, versus AppSync subscriptions after at 0.4 seconds median and 1.1 seconds p95

And because updates are pushed instead of pulled, the request volume on our API tier collapsed:

Bar chart showing status-check API calls per day dropping 94 percent, from 182 million with REST polling to 11 million with AppSync push

The numbers that mattered to the business:

| Metric | Before (polling) | After (AppSync) | |---|---|---| | Median update latency | 7.5 s | 0.4 s | | p95 update latency | 14.2 s | 1.1 s | | Status API calls/day | 182M | 11M (−94%) | | Partner order acceptance time | 45 s avg | 8 s avg | | Mobile battery drain (tracking screen, 30 min) | 6.1% | 2.3% |

The partner acceptance number is the one I care about most. Reaching the partner's app in under a second means the kitchen starts 37 seconds earlier on every order. At 16M orders a year, that's measurable in delivery times and customer ratings, not just dashboards.

Lessons from production

  • Design for reconnects from day one. Mobile networks drop constantly. On reconnect, clients fetch a snapshot via a normal query then resubscribe. Otherwise you miss events that happened during the gap.
  • Keep a polling fallback. Roughly 2% of our sessions run behind networks that break WebSockets. The app silently degrades to 30-second polling; users never know.
  • Watch your subscription auth as carefully as your API auth. Filtering is your tenant isolation. We test subscription rules in CI the same way we test REST authorization.
  • Cost scales with connections and messages, not requests. For us that meant roughly 38% lower cost on this path than the polling fleet it replaced. But model your own message volumes first; a chatty schema can flip that math.

Real-time stopped being a "nice to have" the day we saw partners accepting orders in 8 seconds. If your product has anyone staring at a screen waiting for state to change, push it. Don't make them ask.

Comments

    No comments yet. Be the first to share your thoughts.