The KPI nobody sees
When we start auditing a new client's delivery operation, we always ask the same question: "how long does it take from when the kitchen marks an order as ready to when the courier picks it up?". The answer, in 90% of cases, is "we don't know". Sometimes we hear "less than 2 minutes, normally" — which is the elegant way of saying the same thing.
We call that interval buffer time. And it is the silent killer of food quality and customer experience. While the order sits in the buffer, pizza loses temperature, sauce loses texture, fries lose everything that makes them worth being fries.
Buffer time = "courier picked up" timestamp − "order ready" timestamp. Measured per order, aggregated by store / hour / day.
Why it matters
There is a brutally clear correlation between buffer time and customer NPS. We analyzed ~3.2M Dodo Pizza orders over 18 months and found:
- Buffer < 60s → average NPS +72
- Buffer 60–180s → average NPS +58
- Buffer 180–300s → average NPS +34
- Buffer > 300s → average NPS −12 (annoyed customer)
The 4 mistakes we made
Before we had something decent, we failed four times. Worth documenting so you don't repeat them.
1. Trusting the POS timestamp
Our first instinct was to use order.completed_at from the POS as the "ready" event. Bad idea. In half the stores that field gets filled when the cashier rings up, not when the kitchen finished. The gap can be 90s. Your KPI inherits that bias.
Polling is the way you don't hear about things that matter. If your system supports webhooks or events, use them. If not, instrument it yourself.
from pyspark.sql import functions as F
from delta.tables import DeltaTable
events = (spark.readStream
.format("kafka")
.option("subscribe", "kitchen.events")
.load()
.select(F.from_json("value", schema).alias("e"))
.select("e.*"))
buffer = (events
.groupBy("order_id", F.window("event_ts", "15 minutes"))
.agg(
F.min(F.when(F.col("type") == "ready", F.col("event_ts"))).alias("ready_at"),
F.min(F.when(F.col("type") == "pickup", F.col("event_ts"))).alias("pickup_at"))
.withColumn("buffer_seconds",
F.unix_timestamp("pickup_at") - F.unix_timestamp("ready_at")))
The most surprising thing was not the NPS impact. It was that we found 3 stores where buffer was consistently 6 minutes — same store manager, bad courier scheduling.— Operations Manager, Dodo Pizza Mexico
Results after 6 months
- Median buffer: from 187s to 84s (−55%)
- % of orders with buffer > 5min: from 11.2% to 1.4%
- Average NPS: +18 points in stores with worst initial buffer
- "Cold food" complaints: −63%