SevyDevy

Guide · AI and GenAI

Delivery Time Prediction: Designing a Food-Delivery ETA Model

23 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • Delivery time prediction estimates how many minutes an order will take to reach the customer. For food, that is three waits added up: restaurant prep, rider pickup and the ride to the door.
  • Predict a range, not one number. The p50 is the time by which half of similar orders arrive. The p90 is the time by which 9 in 10 arrive.
  • Good models mix history with live signals, the facts true right now: the restaurant's queue, the rider's GPS position, the rain. Gradient-boosted trees, many small chains of yes-or-no questions where each fixes the errors left by the ones before it, are a strong first choice.
  • Judge the model by MAE (average minutes off) and by coverage, the share of orders that really arrive by their p90 time. Keep the raw prediction separate from the promise customers see.
  • Beginners can stop after the section on ranges. Engineers then get a simulated p90 model that covers about 90 percent of all orders but only 58 percent of rainy ones. They also see why part ranges do not add up.

What goes wrong when a food delivery app shows one fixed ETA?

One fixed formula goes wrong because a food order is three different waits, and each one slows down for its own reason. Delivery time prediction replaces that formula with a model that estimates each wait from history and live signals.

Picture a common setup. A food delivery app in Bengaluru handles about 60,000 orders on a normal day. The setup is made up, but the problem is real.

The app's first ETA (estimated time of arrival, the minutes until food reaches the door) was one line of code. It took the road travel time from a map service and added a fixed 15 minutes for cooking. The app showed one number, such as 28 min.

The operations team reads a month of complaints and late-order coupons. Three patterns stand out.

  • A biryani place in Koramangala takes 25 minutes to pack a big order on Friday nights. The formula always assumed 15.
  • On rainy evenings, 4 in 10 orders arrive after the time shown. Each late order earns a Rs 40 coupon, so 6,000 late orders cost Rs 2.4 lakh (240,000) in one evening.
  • Large gated societies in Whitefield add about five minutes at the gate and in the lift. The formula stopped at the building's pin on the map.

The team wants a time the app can actually keep. The rest of this guide builds it, one part at a time.

What is delivery time prediction?

Delivery time prediction is a machine learning task that estimates how many minutes an order will take, from placing it to food in hand. The most useful answer is a range, not a single number.

Machine learning here means a program that learns patterns from past examples instead of following rules you wrote. The team has millions of past orders, each with the real time it took. The model learns from those.

The three parts of every delivery

Every food order has three parts, and each one is slowed by different things.

Prep. The minutes the restaurant needs to cook and pack the food. It depends on the dishes, the size of the order and how busy the kitchen is right now.

Pickup. The rider rides to the restaurant and collects the food. The food leaves only when both the food and the rider are ready, so the slower of the two decides.

Travel. The ride from the restaurant to the customer, plus the last few minutes at the door. In big apartment blocks, the gate, the lift and finding the flat can add five minutes.

Here is one order from a biryani place in Koramangala to a flat in HSR Layout. Look at the two arrows leaving the first box. Prep and the rider's ride to the restaurant happen at the same time.

One order, part by part

Prep and the rider's ride run side by side. Pickup waits for whichever finishes last.

Diagram connections:

  • Order placed to Restaurant prepares food
  • Order placed to Rider rides to restaurant
  • Restaurant prepares food to Pickup and handover (Food ready)
  • Rider rides to restaurant to Pickup and handover (Rider arrives)
  • Pickup and handover to Ride to the customer
  • Ride to the customer to Food at the door
  • Order placed: Biryani for four, 7:40 pm
  • Restaurant prepares food: Prep: 14 minutes on a busy evening
  • Rider rides to restaurant: 8 minutes away when assigned
  • Pickup and handover: Starts at minute 14, takes 2 minutes
  • Ride to the customer: 4.2 km in evening traffic, 15 minutes
  • Food at the door: Gate and lift add 3 minutes

Add it up for this order. Prep takes 14 minutes and the rider arrives at minute 8, so pickup starts at minute 14. Add 2 minutes of handover, 15 of travel and 3 at the door. The total is 34 minutes, while the old formula showed 28.

Think of a haircut on a busy Saturday. The barber finishing the customer ahead of you is prep. Your own walk to the shop is the rider's ride. The cut starts only when both are done, and the walk home is travel.

Where the picture breaks: a barber's speed barely changes with rain, while roads do. Nobody tracks your walk by GPS either, so your guess cannot update the way an ETA can.

What features does a delivery time model need?

A delivery time model needs features (the input facts it learns from) for each part: how busy the restaurant is, where the rider is, and what the roads are like. The most useful ones are often live signals, not history.

The team's ETA features, grouped by part of the delivery
PartHistory featuresLive signals
PrepRestaurant's median prep time at this hour over 4 weeks, item count, dish typesOrders waiting at the restaurant now, the food-ready tap on the restaurant's tablet
PickupUsual wait at this restaurant, such as a mall food court with slow parkingRider's GPS position and speed, free riders nearby
TravelRoad distance, map service travel time, usual time at this building's gateRain right now, live traffic speed on the route, rider status updates

Live rider signals deserve a closer look. The rider's phone sends a GPS ping (latitude, longitude and speed) every few seconds. It also sends status events: accepted, reached restaurant, picked up, reached customer.

Each status event removes a whole block of uncertainty. Once the rider taps picked up, prep and pickup are known facts, and only the ride and the door are left to predict. That is why each event is a chance to refresh the ETA.

Use only what you knew at that moment

A common trap in ETA training data is leakage: a feature that sneaks in information from the future. Example: training the checkout model on the real pickup time, when no pickup has happened at checkout.

The fix is point-in-time features. For every past order, keep the feature values exactly as they were when each prediction was made. The team logs the features with every prediction, so training data matches what the model really saw.

Which model should you use to predict delivery time?

Start with a simple baseline, then move to gradient-boosted trees, a strong default for tabular data (rows and columns, like a spreadsheet). Deep learning models, neural networks that stack many layers of simple maths operations, are worth trying only after a tuned tree model stops improving.

A baseline is the simplest model you compare everything against. The team's baseline is the restaurant's median prep time plus the map service travel time. If a fancy model cannot beat it, the fancy model is not worth running.

Gradient-boosted trees build many small decision trees, one after another. A decision tree is a short chain of yes-or-no questions, such as whether it is raining. Each new tree learns to fix the errors left by the trees before it.

They handle mixed features and odd patterns well, like a restaurant that is slow only on Fridays. LightGBM, XGBoost and the HistGradientBoostingRegressor in scikit-learn also accept missing feature values without filling them in first. In scikit-learn 1.9.1, the older GradientBoostingRegressor raises an error instead.

Predict the correction, not the whole time

A useful trick is to predict a residual: the gap between a simple estimate and the real time. A routing engine is software that picks a route and adds up the travel time of each road segment.

A 2022 paper on ETA post-processing at scale uses this design. A deep network learns the residual and adds it on top of the routing engine's ETA. The routing engine already knows roads well, so the model learns only what it misses.

A 2023 survey of route and time prediction for instant delivery sorts newer deep models by architecture into sequence-based and graph-based ones. A team at this stage does not need those yet.

The team trains a small model for each part, for the tracking screen, plus one model for the total. The engineer sections below explain why the total needs its own model.

Why predict a delivery time range instead of one number?

Predict a range because delivery time is uncertain, and one number hides how uncertain it is. A range like 30 to 40 minutes tells the customer, and your own code, how much could go wrong.

Two orders can share the same typical time and carry very different risk. A short ride from a quick cafe takes 25 minutes almost every time. A long ride in rain from a busy restaurant also usually takes 25, but sometimes takes 45.

A quantile is the time by which a given share of similar orders arrive. The p50, or median, is the time by which half of them arrive. The p90 is the time by which 9 in 10 arrive. For the HSR Layout order, the model might say p50 is 33 minutes and p90 is 41.

How does quantile regression train a p90 delivery time model?

Quantile regression trains a model to predict a chosen quantile, using the pinball loss, also called quantile loss. It punishes misses on one side more than the other. For the p90, each minute of a late miss costs nine times as much as a minute of an early miss.

Here is the loss for a single order. actual is the real delivery time in minutes, predicted is the model's guess, and q is the quantile, such as 0.9.

python
def pinball_loss(actual, predicted, q):
    """Quantile loss for one order, in minutes."""
    if actual >= predicted:
        return q * (actual - predicted)        # arrived later than predicted
    return (1 - q) * (predicted - actual)      # arrived earlier than predicted

# One order with a p90 guess of 40 minutes
print(round(pinball_loss(46, 40, 0.9), 2))   # 5.4: six minutes late, heavy
print(round(pinball_loss(30, 40, 0.9), 2))   # 1.0: ten minutes early, light

The two return lines are the whole idea. A late miss is multiplied by q, which is 0.9. An early miss is multiplied by 1 minus q, only 0.1. Averaged over many orders, the guess with the lowest loss is the p90.

We checked that last claim on 100,000 simulated delivery times. The guess with the lowest average loss was 40.60 minutes, and the actual p90 was 40.59. Forecasting: Principles and Practice multiplies this loss by 2, which leaves the best guess unchanged.

As of 19 October 2026, scikit-learn 1.9.1, LightGBM 4.7.0 and XGBoost 3.4.1 all take the pinball loss as a setting. XGBoost, since version 2.0, can also learn the p50 and p90 in one model. The team trains one model per quantile, on the same features.

Separately trained quantiles can cross

Two separate models can disagree so badly that the p90 comes out below the p50. We trained both on made-up orders, 10 times each. With 200 training orders, it happened on 2,500 of 200,000 test predictions.

With 5,000 training orders it happened only 44 times, so it matters most where data is thin, such as new restaurants. The promise code later in this guide guards against it.

Part ranges do not add up

The p90 of the total is not, in general, the p90 of prep plus the p90 of travel. The sum can be too wide or too narrow. Which way it goes depends on how each part is spread and how the parts move together.

In one simulation with steady, independent parts, the sum was 44.6 minutes against a true total p90 of 42.4. In another, each part had an 8 percent chance of a 20-minute hold-up. Each part's p90 ignored its hold-up, so the sum was 29.0 against a true 49.0.

How do you evaluate a delivery time model?

Evaluate it with three kinds of numbers: average error, tail error and coverage, which checks that your ranges mean what they say. Then break every number down by zone, hour and weather.

MAE, or mean absolute error, is the average number of minutes the prediction was off, early or late. If three orders were off by 2, 4 and 9 minutes, the MAE is 5. A model trained to lower MAE learns to predict the median.

The p90 absolute error is the miss that 9 in 10 orders stay under. It shows the bad cases that an average hides. A model with an MAE of 5 minutes can still miss by 20 minutes on one order in ten.

Calibration checks whether stated chances are true. If the p90 is right, about 90 percent of orders should arrive by their p90 time. That share is the coverage.

Pause and predict. A p90 model is trained without any rain feature, and 1 in 5 orders happen in rain. Will its overall coverage be near 90 percent?

p90 coverage in our simulation: 40,000 made-up training orders, 10,000 calibration orders, 20,000 test orders, scikit-learn 1.9.1
ModelAll ordersRainy ordersDry orders
p90 model without a rain feature89.8%57.6%97.8%
Same model after a conformal adjustment (explained below)89.9%58.2%97.8%
p90 model with a rain feature89.7%88.7%90.0%

What just happened? The model without rain hit almost exactly 90 percent overall. It did that by over-covering dry orders, which paid for missing more than 4 in 10 rainy ones. Adding the rain feature fixed the rainy segment.

Conformal prediction is a way to adjust any model's ranges using calibration data, a set of held-out orders the model never trained on. If new orders come from the same mix as those, overall coverage is at least what you asked for, on average. One test set can land just under, like the 89.9 percent in the table.

That guarantee is marginal, meaning averaged over all orders, not given for each zone or for rain. Here the overall coverage was already right, so the adjustment moved the p90 by only 0.08 minutes. Rain stayed broken.

Calibrating rainy and dry orders separately, called group-balanced conformal prediction, does fix rain. In our run it lifted rainy coverage to 89.2 percent. It needs rain known at prediction time, and then the model can simply use rain as a feature.

You can check coverage on your own logged predictions with a few lines. Run this and expect two lines of output: all 75% and rain 33%.

python
orders = [  # (actual minutes, p90 shown, raining)
    (31, 38, False), (44, 41, True), (29, 35, False), (47, 42, True),
    (36, 40, False), (39, 41, True), (33, 37, False), (40, 44, False),
]

def coverage(rows):
    """Share of orders that arrived by their p90 time."""
    return sum(actual <= p90 for actual, p90, _ in rows) / len(rows)

print(f"all  {coverage(orders):.0%}")                          # all  75%
print(f"rain {coverage([r for r in orders if r[2]]):.0%}")     # rain 33%

Next the team runs an A/B test for two weeks, where some customers see the new ETA and others the old one. The operations team watches late coupons, cancellations and conversion, the share of people who open a menu and then order.

How do you turn a predicted ETA into the promise customers see?

A separate promise layer turns the model's quantiles into what the customer sees, using business rules the operations team owns. The model says what will probably happen. The promise is what the app is willing to commit to.

Being late usually costs more than being early. A late order brings a coupon and an angry customer, while an early one is a pleasant surprise. The 2022 post-processing paper allows for this: its loss can weight under-prediction and over-prediction differently.

But a promise that is too long loses orders. If the app says 45 to 55 minutes, people pick a nearer restaurant. Operations balances late coupons against lost orders, and that choice is theirs, not the model's. Here are the team's promise rules.

  • Show a range: the p50 rounded to the nearest 5 minutes, and the p90 rounded up to the next 5.
  • Never show a range narrower than 5 minutes, or a start earlier than 10 minutes.
  • After checkout, move the promise only on a rider event, or when the new prediction differs by 3 minutes or more.
  • When the promise moves later, show the reason on screen, such as heavy rain or a busy restaurant.
  • Log the raw p50, the raw p90 and the promise shown, side by side, for every order.

Here are the first two rules in code, taking the model's p50 and p90 in minutes.

python
import math

def promise_window(p50, p90):
    """Turn model quantiles (minutes) into the range shown at checkout."""
    low = max(10, 5 * round(p50 / 5))    # nearest 5, at least 10
    high = 5 * math.ceil(p90 / 5)        # always round the top up
    high = max(high, low + 5)            # at least 5 minutes wide
    return low, high

print(promise_window(32.4, 38.9))   # (30, 40)

The line that matters most is the math.ceil one. Rounding the p90 down would quietly turn a 90 percent promise into a weaker one. Operations can later change the 5 or the 10 without anyone retraining the model.

The max(high, low + 5) line also guards against crossed quantiles. Given a p50 of 30 and a p90 of 29, it still returns 30 to 35. One Python detail: round sends exact halves to the even number, so a p50 of 32.5 shows as 30, not 35.

How do you serve and refresh ETA predictions in real time?

Serve ETAs from a fast prediction service that reads live features from an online feature store, a fast database of the latest feature values. Recompute the ETA whenever a rider event arrives. Retrain on a schedule, from logged predictions joined with real delivery times.

The app asks for an ETA at three moments, and each has different needs.

  • Browsing: a list page shows 20 restaurants at once. These ETAs use a lighter model, cached per restaurant and area for one minute.
  • Checkout: the full model runs once per order, within a latency budget (the most time it may take) of about 50 milliseconds (made up).
  • Tracking: after the order, the ETA is recomputed on every rider event, and every two minutes in between.

The online feature store holds values like a restaurant's queue right now. A stream processor is a worker that reads events as they arrive and updates those values.

Here is the serving setup. Follow the rider events into the feature store, then follow the prediction log back into training.

How the team serves and retrains its ETA model

Live features flow in from riders. Every prediction is logged, joined with the real time, and used to train the next model.

Diagram connections:

  • Customer app to ETA service (Ask for ETA)
  • Rider event stream to Stream processor
  • Stream processor to Online feature store (Update features)
  • ETA service to Online feature store (Read live features)
  • ETA service to Prediction log (Log every prediction)
  • Prediction log to Weekly training job
  • Weekly training job to Model registry (Save new version)
  • Model registry to ETA service (Load approved model)
  • Prediction log to Monitoring
  • Customer app: Asks for ETAs at browse, checkout, tracking
  • ETA service: Runs the model and the promise rules
  • Rider event stream: GPS pings and status events
  • Stream processor: Turns events into fresh feature values
  • Online feature store: Latest restaurant queue, rider spot, rain
  • Prediction log: Features, p50, p90, promise, model version
  • Weekly training job: Joins the log with real delivery times
  • Model registry: Stores each approved model version
  • Monitoring: Coverage and error by zone and hour

When an order is delivered, its real time is joined to the logged prediction. That joined table feeds next week's training and the monitoring.

The team retrains weekly on the last 12 weeks of orders. Each version is saved in a model registry, a store of trained models. A new model first runs in shadow mode: it predicts on live traffic, but customers still see the old model's ETA.

It goes live only if its MAE and coverage, split by segment, beat the current model. Refreshing during tracking needs care too, because a number that jumps every few seconds feels broken. Look at the question box below.

Refreshing the ETA during tracking

Every event triggers a new prediction, but the screen changes only for a real difference.

Diagram connections:

  • Rider event arrives to Read live features
  • Read live features to Predict new p50 and p90
  • Predict new p50 and p90 to Log the prediction
  • Predict new p50 and p90 to Changed by 3 min or more?
  • Changed by 3 min or more? to Update tracking screen (Yes)
  • Changed by 3 min or more? to Keep the shown time (No)
  • Rider event arrives: Picked up, or the 2-minute timer
  • Read live features: Rider position, traffic, rain
  • Predict new p50 and p90: Same model as checkout
  • Log the prediction: Kept even when not shown
  • Changed by 3 min or more?: Compared with the time on screen
  • Update tracking screen: With a reason if it moved later
  • Keep the shown time: Stops the number from jittering

Every new prediction is logged, even when the screen keeps the old time. Those hidden predictions still feed monitoring, so you can see how often the 3-minute rule held a number back.

How do you monitor delivery time predictions after launch?

Monitor coverage and error every day, split by zone, hour, weather and restaurant type, because an ETA model can be fine on average and broken for one area. Watch the inputs too, not only the outputs.

  • Coverage by segment: alert when p90 coverage in any zone stays below 85 percent for a full day.
  • Error drift: compare this week's MAE with the last four weeks, hour by hour.
  • Feature health: alert when a live feature stops updating, such as restaurant tablets sending no food-ready taps.
  • New restaurants: with no history, fall back on averages for the same cuisine and area, and keep the range wider until enough orders come in.
  • Special days: the first monsoon rain, a cricket final or Diwali week. Check those days by hand.

How do you explain delivery time prediction in a system design interview?

Walk through it as a pipeline, from what the ETA is for to how you keep it honest after launch. Say the trade-off at each step.

  1. Clarify the use

    Ask where the ETA appears: list page, checkout or tracking. Ask what a late order costs and what the latency budget is.

  2. Split the time

    Break the total into prep, pickup and travel. Point out that prep and the rider's ride to the restaurant run in parallel.

  3. Name the features

    Give history and live signals for each part. Mention point-in-time features, so no future information leaks into training.

  4. Pick a model

    Start from a baseline, then gradient-boosted trees. Consider predicting a residual on top of the routing engine's estimate.

  5. Predict a range

    Train a p50 and a p90 with quantile loss. Explain why part ranges cannot simply be added together.

  6. Evaluate

    Use MAE, p90 absolute error and coverage, split by segment. Then run an A/B test on late rate and orders.

  7. Serve and monitor

    Describe the feature store, event-driven refresh, the promise layer, shadow mode and coverage alerts.

Questions people ask

What is the difference between ETA and delivery time?
They are two views of the same estimate. Delivery time is the total duration from order to handover, while ETA is the time left until arrival at any moment. A model can be trained on total delivery time, while the tracking screen shows the time left.
Which algorithm is best for delivery time prediction?
No single algorithm is best, but gradient-boosted trees are a strong first choice for tabular features like distance, prep history and rain. Deep sequence and graph models appear in recent research. Try them only once a tuned tree model has stopped improving.
What is quantile regression in simple words?
Quantile regression predicts a chosen percentile of the outcome instead of the average. For delivery, a p90 model predicts the time that 9 in 10 similar orders beat. It is trained with the pinball loss, which punishes misses on one side more than the other.
How do you predict delivery time for a new restaurant with no history?
Use fallback features from similar restaurants, such as the median prep time for the same cuisine in the same area. As real orders arrive, blend in the restaurant's own history. Keep its range wider until there are enough orders to trust, and check its coverage separately.
Why use MAE instead of RMSE for ETA models?
MAE is in minutes and easy to explain: the average number of minutes the prediction was off. A model that minimises MAE predicts the median, which suits skewed outcomes like delivery time. RMSE (root mean squared error) squares each miss before averaging, so big misses count far more. Minimising it leads to the mean. Prefer it when big misses matter most, or when ETAs are added or averaged.
How often should an ETA model be retrained?
There is no fixed rule. Weekly retraining on the last few months of orders is a reasonable starting point, but measure what works for you. Retrain sooner when coverage drops after a change, like a new zone or the monsoon. Always shadow test a new model before customers see it.
Can a delivery time model use rider GPS data?
Yes. Once the rider is moving, live position tells the model how much of the trip is left, which history alone cannot. Store each ping with a timestamp, smooth noisy readings, and handle phones that stop sending. Treat location as personal data: keep only what you need, for a limited time.

Sources and further reading

Practise this with Lala

Practise