El software suele imaginar el tiempo como un número que siempre crece de forma perfecta. Producción no es así. Los relojes se desincronizan, los jobs se ejecutan tarde, los mensajes llegan fuera de orden, los retries duplican trabajo y los despliegues detienen workers en el peor momento. Diseñar sistemas basados en tiempo exige tratar el reloj como una dependencia imperfecta y definir qué debe ocurrir cuando una operación llega temprano, tarde o más de una vez.

Elige el reloj según la pregunta

Wall clock responde “qué hora era en el mundo compartido”. Monotonic clock responde “cuánto duró esta operación dentro de esta máquina”. Medir timeout con wall clock puede fallar si el reloj se corrige hacia atrás. Registrar auditoría con monotonic time no sirve fuera del proceso.

El código debe separar esas fuentes. Deadlines derivados de duraciones usan monotonic clock; eventos persistidos usan un instant UTC documentado. Esa separación reduce bugs difíciles de reproducir.

La expiración es una política

Comparar now > expires_at parece suficiente hasta que aparece clock skew o una request cruza el límite durante procesamiento. Un token de seguridad puede permitir unos segundos de tolerancia; una reserva limitada quizá requiera enforcement transaccional en base de datos.

Define si el límite es inclusivo, qué reloj es autoritativo y cuánto grace period existe. Todos los servicios deben aplicar la misma regla para que el usuario no reciba decisiones contradictorias.

Los jobs programados no se ejecutan exactamente a tiempo

Un scheduler puede hacer disponible un trabajo a medianoche, pero workers ocupados, downtime o dependencia caída pueden retrasarlo. El job debe saber si ejecutarse tarde aún tiene sentido. En algunos casos debe ponerse al día; en otros debe saltarse o compensar.

Los recurring jobs necesitan política de missed runs. Ejecutar todas las ocurrencias perdidas puede saturar el sistema; ignorarlas puede perder datos. La decisión es de negocio y debe estar documentada.

La idempotencia es obligatoria

Queues y schedulers pueden entregar el mismo trabajo más de una vez. Un timeout puede disparar retry aunque la primera ejecución haya tenido éxito. Sin idempotencia, se envían emails duplicados, se crean facturas repetidas o se aplican transferencias dos veces.

Un operation ID estable, constraints únicas o estado registrado permiten repetir de forma segura. Exactly-once entre sistemas es difícil; safely retryable suele ser más realista.

Timestamp no garantiza orden causal

Dos servicios pueden tener relojes distintos. Una operación temprana puede llegar tarde por red. Ordenar eventos solo por timestamp produce una historia plausible pero no necesariamente verdadera. Para concurrencia y decisiones críticas se necesitan sequence numbers, versiones de dominio o transacciones.

El timestamp sigue siendo útil para observación y diagnóstico. Simplemente no debe convertirse en mecanismo oculto de control de concurrencia.

Inyecta el tiempo en la lógica

Código que llama al reloj real en cada función es difícil de probar. Pasar un clock o current instant como dependencia permite cubrir límites de expiración, daylight saving y retries sin sleeps. Los tests se vuelven deterministas y rápidos.

Un fake clock debe avanzar por decisión del test, no por espera real. Así se pueden probar horas, días o vencimientos instantáneamente.

Las fronteras críticas requieren transacción

Para reservas, leases o inventario limitado, verificar tiempo en application code y actualizar después crea carreras. La base de datos o sistema autoritativo debe comparar deadline y cambiar estado en una operación atómica.

Los efectos externos siguen necesitando idempotencia y reconciliación. La transacción protege estado interno, pero no garantiza que un proveedor de correo o pagos actúe exactamente una vez.

Reconciliation repara supuestos rotos

Incluso un buen scheduler necesita procesos que encuentren trabajos vencidos, estados atascados y operaciones incompletas. Reconciliation convierte fallos temporales en demoras recuperables. Debe ser seguro ejecutarlo repetidamente.

Diseñarlo desde el inicio es más fácil que intentar demostrar que ningún timing path fallará. Los sistemas distribuidos siempre encuentran una forma de retrasarse.

Observa el tiempo con métricas específicas

Queue depth no dice cuánto se retrasó el trabajo. Mide schedule lag, queue wait, execution duration, edad del elemento más antiguo y porcentaje de deadlines incumplidos. Cada métrica apunta a una causa distinta.

El objetivo de servicio debe definir lateness aceptable. Un minuto puede ser crítico para un lease de autorización y trivial para un reporte nocturno. El diseño confiable empieza aceptando esa diferencia.

También registra decisiones temporales importantes: por qué expiró un acceso, qué reloj se usó, qué grace period aplicó y qué attempt ejecutó un job. Esa información convierte incidentes confusos en diagnósticos verificables.

Los sistemas maduros tratan el tiempo como una entrada externa con incertidumbre. Diseñan retries con deadline, trabajos idempotentes, reconciliación periódica y métricas que muestran retrasos antes de que se transformen en pérdida de datos o mala experiencia.

El resultado no es un sistema que nunca llegue tarde, sino uno que sabe cuánto se retrasó, si todavía debe actuar y cómo recuperarse sin duplicar efectos externos peligrosos.