El KPI que nadie ve
Cuando empezamos a auditar la operación de delivery de un cliente nuevo, hacemos siempre la misma pregunta: "¿cuánto tiempo pasa entre que la cocina marca un pedido como listo y el courier lo recoge?". La respuesta, en el 90% de los casos, es "no sabemos". A veces escuchamos "menos de 2 minutos, normalmente", que es la forma elegante de decir lo mismo.
A ese intervalo lo llamamos buffer time. Y es el asesino silencioso de la calidad de la comida y de la experiencia del cliente. Mientras el pedido está en el buffer, la pizza pierde temperatura, la salsa pierde textura, las papas pierden todo lo que hace que valga la pena ser papa frita.
Buffer time = timestamp de "courier picked up" − timestamp de "order ready". Se mide por pedido y se agrega por local, hora, día.
Por qué importa tanto
Existe una correlación brutalmente clara entre buffer time y puntaje NPS del cliente. En los datos de Dodo Pizza analizamos ~3.2M pedidos durante 18 meses, y encontramos lo siguiente:
- Buffer < 60s → NPS promedio +72
- Buffer 60–180s → NPS promedio +58
- Buffer 180–300s → NPS promedio +34
- Buffer > 300s → NPS promedio −12 (cliente molesto)
Los 4 errores que cometimos
Antes de tener algo decente, fallamos cuatro veces. Vale la pena documentarlos para que no los repitas.
1. Confiar en el timestamp del POS
El primer impulso fue usar order.completed_at del POS como evento "listo". Mala idea. En la mitad de los locales ese campo se llena cuando el cajero cobra, no cuando la cocina terminó. La diferencia puede ser 90s. Tu KPI hereda ese sesgo.
2. Usar polling cada 30 segundos
La primera versión del pipeline corría un cron cada 30s que consultaba la API del KDS. Para 100 locales eso fueron 12,000 requests por hora — y aún así perdíamos eventos cuando dos órdenes pasaban a "listo" en la misma ventana.
Polling es la forma de no enterarte de cosas que importan. Si tu sistema soporta webhooks o eventos, úsalos. Si no, instrumenta tú mismo.
Arquitectura final
Después de iterar, llegamos a un setup que ya lleva 4 años en producción sin cambios estructurales. Tres capas:
- Captura: KDS → webhook HTTPS → Kafka topic
kitchen.events - Procesamiento: Spark Streaming consume Kafka, deduplica por
event_id, escribe a Delta Lake (bronze → silver → gold) - Servir: Superset lee la tabla gold, alertas a Telegram via webhook
Lo más sorprendente no fue el impacto en NPS. Fue que descubrimos 3 locales donde el buffer estaba en 6 minutos consistentemente. Resultó ser el mismo gerente, mala asignación de couriers.— Operations Manager, Dodo Pizza México
Código que usamos
from pyspark.sql import functions as F
from delta.tables import DeltaTable
# 1. Read events from Kafka
events = (spark.readStream
.format("kafka")
.option("subscribe", "kitchen.events")
.load()
.select(F.from_json("value", schema).alias("e"))
.select("e.*"))
# 2. Pivot ready / pickup events per order
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")))
# 3. Upsert to Delta gold table (idempotent)
def upsert(batch_df, batch_id):
DeltaTable.forName(spark, "gold.buffer_time_per_order") \
.merge(batch_df.alias("new"), "new.order_id = old.order_id") \
.whenMatchedUpdateAll().whenNotMatchedInsertAll().execute()
Resultados después de 6 meses
Después de instrumentar buffer time en 100+ locales y darle a cada gerente regional acceso al dashboard, los números cambiaron:
- Buffer mediano: de 187s a 84s (−55%)
- % de pedidos con buffer > 5min: de 11.2% a 1.4%
- NPS promedio: +18 puntos en los locales con peor buffer inicial
- Quejas por "comida fría": −63%
Conclusión
Si operás delivery y no estás midiendo buffer time, lo estás perdiendo. Los KPIs que importan rara vez son los que tu dashboard te muestra por defecto — son los que tenés que construir explícitamente porque conoces el dominio.