MMMiller Millán Caso de estudio
Caso de estudio · Logística & optimización

¿Cómo optimicé rutas de reparto con cadena de frío y ventanas de horario? (VRP real, no "ordenar por distancia")

Por Miller Millán 18 de junio, 2026 8 min de lectura

Respuesta corta: no se resuelve ordenando paradas por cercanía ni tirando las direcciones a Google Maps. El reparto real tiene tres restricciones que rompen ese enfoque —capacidad del camión, vehículos refrigerados y ventanas de horario por cliente— y eso es, literalmente, un Vehicle Routing Problem. Lo resolví montando un solver de VRP de verdad (VROOM) sobre una matriz de distancias reales por carretera (OSRM), los dos self-hosted en Docker, con un optimizador propio como fallback. Abajo cuento por qué, y por qué lo obvio no alcanza.

El problema: "armar las rutas del día" parece fácil hasta que no lo es

En TrabaJIA armé el módulo de logística para una operación de distribución de alimentos con cadena de frío: cada mañana hay decenas de pedidos, una flota de camiones —algunos refrigerados, otros no— y hay que decidir qué camión lleva qué, en qué orden.

La trampa es que esto parece un problema de "ordenar por distancia". Lo es solo si todos los pedidos caben en cualquier camión, todos los clientes reciben a cualquier hora y nada se echa a perder. En el reparto real, ninguna de esas tres cosas es cierta:

Ordenar por distancia ignora las tres. Por eso lo obvio no alcanza.

La decisión: tratarlo como lo que es — un Vehicle Routing Problem

En vez de inventar mi propia "lógica de ordenar paradas", asumí que esto es un problema clásico y bien estudiado: el VRP (Vehicle Routing Problem), y más específicamente sus variantes con capacidad (CVRP), ventanas de tiempo (VRPTW) y habilidades (Skill-VRP, para el frío).

Monté dos piezas, las dos self-hosted en Docker:

Y dejé el motor intercambiable con una variable de entorno (MOTOR_RUTAS): si está en vroom, usa VROOM+OSRM; si no, cae a un optimizador propio (una heurística de ~30 KB que escribí) que usa Google Routes solo para dibujar la polilínea final de la ruta.

El detalle de arquitectura del que estoy más orgulloso: el adaptador de VROOM (vroomOptimizer.ts) devuelve exactamente la misma estructura que mi optimizador local. Misma forma de salida. Eso significa que puedo cambiar el cerebro del ruteo sin tocar la UI ni el resto del flujo. Cambias una variable de entorno y listo.

Cómo funciona: las restricciones, traducidas a un modelo

Acá está el corazón. Cada pedido y cada camión se traducen a un modelo que el solver entiende:

1. Capacidad multidimensional. No le digo al solver "este pedido pesa X". Le doy un vector de tres dimensiones y lo optimiza contra las tres a la vez:

amount:   [pesoKg, canastas, piezasGrandes]   // el pedido
capacity: [capacidadKg, capacidadCanastas, capacidadPiezas]  // el camión

El camión se llena cuando se agota cualquiera de las tres. Así modelas la realidad: el camión que se quedó sin canastas no puede llevar más, aunque le sobre peso.

2. Cadena de frío como "habilidad" (Skill-VRP). Esto es elegante por lo simple. Los pedidos que necesitan frío llevan una habilidad requerida; los camiones refrigerados, esa misma habilidad disponible:

if (pedido.requiereRefrigeracion) job.skills = [1];   // el pedido la exige
if (vehiculo.refrigerado)         vh.skills  = [1];    // el camión la tiene

El solver nunca le asigna un pedido refrigerado a un camión sin frío. Es matemáticamente imposible que pase, no depende de que nadie se acuerde. En mi fallback propio la misma regla es una línea: if (requiereRefrigeracion && !v.refrigerado) return false; — ese camión queda descartado para ese pedido, punto.

3. Ventanas de tiempo por cliente (VRPTW). Cada parada puede llevar su franja horaria:

job.time_windows = [[ventanaInicio, ventanaFin]];   // p. ej. 6:00 a 9:00

Y acá mi optimizador propio hace algo que me gusta: si el camión llega antes de que abra la ventana, espera (no entrega a una puerta cerrada); si llega después de que cierra, lo penaliza fuerte; y deja 30 minutos de margen de seguridad antes del cierre, porque "llegar justo" en logística es llegar tarde.

4. Tiempo de servicio y utilización máxima. Cada parada consume minutos (bajar, entregar, firmar), y puedes decirle a un camión que no use el 100 % de su capacidad sino, digamos, el 85 % —para dejar aire.

Y todo esto corre bajo un modelo dinámico diario, estilo Amazon: cada día se optimiza de cero con los camiones que ese día están disponibles. Nada de territorios fijos ni "esta zona es de tal conductor". El que está hoy, con los pedidos de hoy, en las rutas óptimas de hoy.

La historia de guerra: el hotel de las 6 a 9 am

El caso que me hizo entender que esto no se resuelve "a ojo" fue real y está hasta comentado en mi código.

Había un hotel que solo recibe de 6 a 9 de la mañana. Una ventana durísima. Y el ruteo, optimizando por eficiencia de distancia, lo metía cómodo en la posición 10 de la ruta —porque geográficamente quedaba bien ahí. Solo que para cuando el camión llegaba a la parada 10, eran las 11 de la mañana. Ventana cerrada. Entrega fallida. Viaje perdido y mercadería que se vuelve.

La eficiencia pura de distancia te traiciona: la ruta más corta en kilómetros puede ser la peor en cumplimiento. La solución no fue "ponerlo primero a mano" —eso no escala a decenas de clientes con franjas distintas—. Fue meter la urgencia de la ventana dentro del puntaje del optimizador: cuanto más pronto cierra la ventana de un cliente y menos margen queda, más prioridad gana esa parada. El hotel dejó de ser "la parada 10 eficiente" para ser "la primera, porque si no, no hay entrega".

En logística de cadena de frío, la ruta óptima no es la más corta: es la que llega a tiempo, en frío y con el camión bien lleno. Los kilómetros son lo último que optimizas, no lo primero.

Por qué NO hice lo obvio

Esta es la parte que casi nadie cuenta, y la razón por la que valió la pena la infraestructura.

Por qué no "ordené por distancia" (vecino más cercano). Es el primer impulso de todo el mundo: empieza por la planta, ve siempre a la parada más cercana. Es trivial de programar y está mal para reparto real, porque ignora capacidad (te llena el camión sin pensar), ignora la cadena de frío (te manda un congelado en un camión caliente) e ignora las ventanas (te deja al hotel de las 6 am para el mediodía). Optimiza la única variable que menos importa.

Por qué no tiré todo a Google Maps. Google Maps optimiza el orden de unas pocas paradas en un vehículo. No sabe nada de tu flota: no sabe cuáles camiones tienen frío, no entiende canastas ni piezas, no reparte la carga entre varios vehículos según capacidad. Google te ordena paradas; el VRP te asigna pedidos a camiones Y los ordena respetando restricciones. Son problemas distintos. Y depender de una API externa de pago para el core de tu operación, con cuotas y costos por llamada, es atarte de una forma que no quería.

Por qué self-hosted (VROOM + OSRM en Docker). Lo levanté yo en mi VPS por tres razones: costo (no pago por optimización ni por cada matriz de distancia), privacidad (las direcciones de los clientes no salen a un tercero) y control (puedo afinar el modelo, agregar dimensiones, cambiar restricciones). El fallback propio + Google Routes existe justamente para no quedar atado a una sola pieza: si VROOM no está, el sistema sigue ruteando.

El resultado

Hoy la plataforma, cada mañana, toma los pedidos del día y la flota disponible y arma las rutas sola: reparte la carga respetando peso/canastas/piezas, garantiza que ningún pedido refrigerado caiga en un camión sin frío, y ordena cada ruta para cumplir las ventanas de horario —esperando si llega temprano, priorizando al que cierra pronto. Lo que antes era criterio de una persona con un mapa mental, ahora es un modelo que considera todas las restricciones a la vez, todos los días, sin territorios fijos.

Y porque el motor es intercambiable, puedo evolucionar el cerebro del ruteo sin reescribir la operación encima.

Lo que aprendí

El reparto "parece" un problema de mapas y es un problema de restricciones. La distancia es la parte fácil y la menos importante. Lo difícil —y lo que define si la operación funciona— es a quién no se le puede llegar tarde, qué no puede ir sin frío, y cuánto cabe de verdad en cada camión. Cuando reconoces que eso es un VRP de libro y dejas de inventar tu propia "lógica de ordenar paradas", el problema pasa de imposible a modelable.

Cualquiera ordena paradas por cercanía. El reparto real lo ganas resolviendo lo que la cercanía ignora: capacidad, frío y la hora a la que el cliente abre la puerta.

Preguntas frecuentes

¿Por qué no alcanza con "ordenar las paradas por la más cercana"?

Porque el vecino más cercano optimiza solo distancia e ignora las tres restricciones que rompen el reparto real: la capacidad del camión (peso, canastas y piezas), la cadena de frío (qué pedido necesita un camión refrigerado) y las ventanas de horario del cliente. La ruta más corta en kilómetros puede ser la que más entregas falla.

¿Google Maps no resuelve esto?

No del todo. Google Maps optimiza el orden de unas pocas paradas en un solo vehículo. No conoce tu flota: no sabe cuáles camiones tienen frío, no entiende tus unidades de carga, ni reparte los pedidos entre varios camiones por capacidad. Eso es un Vehicle Routing Problem, y para eso usas un solver de VRP (yo uso VROOM) sobre distancias reales por carretera (OSRM).

¿Qué es VROOM y qué es OSRM, y por qué los dos?

OSRM calcula distancias y tiempos reales de manejar entre puntos (la matriz). VROOM es el solver que, con esa matriz, decide qué camión lleva qué pedidos y en qué orden, respetando capacidad, habilidades (frío) y ventanas de tiempo. Uno da los costos de viajar; el otro toma las decisiones. Los corro self-hosted en Docker por costo, privacidad y control.

¿Cómo se modela la cadena de frío en un VRP?

Como una habilidad (Skill-VRP): el pedido que necesita refrigeración declara una habilidad requerida y solo los camiones refrigerados declaran tenerla. El solver nunca puede asignar ese pedido a un camión sin frío —no es una regla que alguien tenga que recordar, es imposible por construcción del modelo.

¿Y si un cliente solo recibe en cierto horario?

Le pones una ventana de tiempo (VRPTW). El ruteo prioriza por urgencia de esa ventana: si llega antes de que abra, espera; si la ventana cierra pronto, esa parada sube de prioridad; y conviene dejar un margen de seguridad (yo uso 30 minutos) antes del cierre, porque llegar justo es llegar tarde.

Miller Millán

Soy Miller Millán, fundador de TrabaJIA, una plataforma multi-empresa de gestión de flotas, activos retornables y logística. Construyo software real para operaciones reales —con IA y optimización aplicadas donde de verdad suman. ¿Estás resolviendo algo parecido? Escríbeme.