DateTime.Now devuelve la hora actual tal como la marca el reloj de la máquina. DateTime.UtcNow devuelve el mismo instante expresado en UTC. En una máquina que va dos horas por delante de UTC, los dos valores se diferencian en dos horas, pero ambos describen el mismo momento.

La recomendación en pocas palabras: almacena UtcNow y muestra Now. Todo lo demás en este artículo es el razonamiento que hay detrás de esa frase y las trampas que aparecen cuando un valor pasa a otra máquina, entra en una base de datos o atraviesa un cambio de horario de verano.

Para descargar el código fuente de este artículo, puedes visitar nuestro repositorio de GitHub.

Empecemos.

¿Qué diferencia hay entre DateTime.Now y DateTime.UtcNow?

DateTime.Now devuelve la fecha y hora actuales de la máquina que ejecuta el código, expresadas en la hora local de esa máquina. DateTime.UtcNow devuelve el mismo instante expresado en tiempo universal coordinado. En una máquina que va dos horas por delante de UTC, los dos valores están separados por dos horas y ambos son correctos.

La diferencia que importa en el código no es el número, sino el indicador que viaja con él. Now devuelve un valor cuyo Kind es Local; UtcNow devuelve uno cuyo Kind es Utc. Cada conversión posterior lee ese indicador para decidir si ajusta el valor o lo deja como está.

Además, Now es el más costoso de los dos. Lee el mismo reloj UTC subyacente y después aplica el desplazamiento (offset) de la zona horaria local, incluida la regla de horario de verano que estuviera vigente en ese instante, así que hace estrictamente más trabajo que UtcNow.

La consecuencia práctica: UTC es la opción segura por defecto para todo lo que se almacena, se compara o se envía a través de una frontera.

UTC es un formato universal para representar la fecha y la hora como alternativa a la hora local. También se conoce como la zona horaria GMT+00.

Veamos un ejemplo para entender mejor la diferencia:

var now = DateTime.Now;
var utcNow = DateTime.UtcNow;

Console.WriteLine($"Local Now: {now}");
Console.WriteLine($"UTC Now: {utcNow}");

Ahora, ejecutemos la aplicación:

Local Now: 8/30/2026 11:52:52 AM
UTC Now: 8/30/2026 9:52:52 AM

El primer mensaje en pantalla varía según nuestra hora local. La hora local de cada sistema depende de la zona horaria asignada. En la mayoría de los casos la dejamos con el valor predeterminado, que corresponde a la zona geográfica en la que vivimos (en nuestro caso, UTC+2), pero también podemos establecer un valor personalizado si hace falta. Aquí ambos valores se imprimen con el ToString() predeterminado; tratamos por separado cómo dar formato al valor una vez que lo tenemos, junto con las operaciones aritméticas que admiten los valores DateTime.

El segundo mensaje está en formato UTC. Muestra la fecha y hora actuales en UTC, es decir, en la zona horaria GMT+00. Siempre podemos considerar UTC como la base para calcular todas las demás zonas horarias, como un formato independiente de la zona horaria:

Un mismo instante mostrado de tres formas: UTC 09:52:52 con Kind Utc, hora local 11:52:52 con Kind Local y 11:52:52 con Kind Unspecified, sin desplazamiento.

Aquí vemos que la hora local de la máquina va 2 horas por delante de UTC. Los dos primeros valores son el mismo momento escrito de dos formas. El tercero muestra las mismas cifras sin el indicador, que es el caso con el que en realidad se encuentra la mayor parte del código.

¿Cuál conviene usar: DateTime.Now o DateTime.UtcNow?

Usa DateTime.UtcNow para todos los valores que guarda la aplicación: columnas de base de datos, marcas de tiempo de los logs, registros de auditoría, caducidad de la caché, vigencia de los tokens y cualquier valor que cruce el límite de un proceso. UTC no tiene desplazamiento que perder ni cambios de horario de verano, así que dos valores registrados con una hora de diferencia están realmente a una hora de distancia.

Usa DateTime.Now solo en el extremo, donde una persona lee el resultado. Convierte a partir del valor UTC almacenado en el momento de mostrarlo, con la zona horaria del lector y no con la que resulte tener el servidor.

La regla se sostiene en despliegues reales porque las máquinas cambian. El código que almacena DateTime.Now produce un historial en la máquina de un desarrollador y otro distinto en un servidor configurado en UTC, a partir del mismo código fuente y sin ningún error en ninguna parte.

The Web API Production Checklist, ebook gratuito en inglés

Ebook gratis

¿Tu Web API está lista para producción?

33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.

El ebook está en inglés.

Descarga la checklist gratis

PDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.

Cuando de verdad hay que conservar el desplazamiento junto con el instante en lugar de descartarlo, DateTimeOffset es el tipo más adecuado para la tarea.

Para los casos en los que el propio desplazamiento es un dato y no ruido, tratamos por separado DateTimeOffset y en qué se diferencia de DateTime.

SituaciónA qué llamarPor qué
Escribir un valor en una base de datosDateTime.UtcNowNo hay desplazamiento que perder, ni huecos ni horas repetidas en un cambio de horario de verano
Poner la marca de tiempo a una entrada de log o de auditoríaDateTime.UtcNowLas entradas se pueden seguir ordenando entre servidores de zonas horarias distintas
Devolver una hora en una respuesta de APIDateTime.UtcNowNo le corresponde al servidor adivinar la zona horaria del consumidor
Comparar dos instantes almacenadosValores DateTime.UtcNowDos valores Local capturados en zonas distintas no son comparables
Mostrar una hora a una personaDateTime.Now, o convertir el valor UTC almacenadoEl único caso en que la hora local es la respuesta correcta
Medir cuánto tardó algoNinguno de los dos; usa StopwatchLa hora de reloj puede dar saltos; un reloj de tiempo transcurrido, no
Conservar el desplazamiento junto con el instanteDateTimeOffsetRegistra el momento y el desplazamiento con el que se capturó
Obtener la hora dentro de código testeableTimeProvider.GetUtcNow()Una propiedad estática no se puede sustituir fácilmente en una prueba

Dos temas relacionados tienen sus propios artículos: Stopwatch, para medir el tiempo transcurrido, y DateOnly y TimeOnly, cuando solo importa una mitad del valor.

¿Cómo saber si un DateTime es UTC o local?

Lee la propiedad Kind. DateTime.Kind devuelve un valor DateTimeKind que es Local, Utc o Unspecified, y es lo único dentro de la estructura que deja constancia de cuál de los tres debía ser cada valor.

Unspecified es a la vez el caso habitual y el peligroso. Es lo que lleva un DateTime cuando se obtiene al convertir una cadena sin zona horaria, cuando se lee de una columna de base de datos que no almacena desplazamiento o cuando se crea con new DateTime(2022, 1, 9). El valor parece completo, pero el indicador no dice absolutamente nada.

El indicador tampoco forma parte del valor. Dos valores DateTime con los mismos ticks y distinto Kind se consideran iguales al compararlos, y algunos serializadores descartan el indicador a la salida o se inventan uno al volver a entrar.

Así que Kind responde a la pregunta de forma fiable solo para un valor que nunca ha salido del proceso. En cualquier otro caso, la respuesta tiene que venir de una convención que el código establece y documenta.

El caso de la conversión desde una cadena es el que encontramos con más frecuencia, así que vale la pena saber exactamente qué obtenemos al convertir una cadena en un DateTime, que es de donde salen los valores Unspecified.

Al desarrollar aplicaciones, podríamos recibir valores DateTime de recursos externos que no controlamos. Puede que no sepamos qué formato se usó antes de la transferencia.

Supongamos que recibimos el valor 8/30/2026 9:52:52 AM. Si no sabemos mucho del remitente, este valor podría ser cualquier cosa: podría venir de un servidor en UTC+02, de uno en UTC-03 o de cualquier otra zona horaria. En este caso, la propiedad DateTime.Kind del valor convertido es Unspecified.

La propiedad DateTime.Kind indica si un valor DateTime se representa como hora local, como UTC o como ninguna de las dos. Esta propiedad devuelve un valor de la enumeración DateTimeKind de C#, y sus valores posibles son Local, Utc o Unspecified.

Veamos un ejemplo de cómo comprobar el formato de una fecha:

var now = DateTime.Now;
var utcNow = DateTime.UtcNow;

Console.WriteLine(now.Kind);
Console.WriteLine(utcNow.Kind);

Esto debería mostrar el Kind Local y después el Kind Utc.

¿Cómo convertir entre hora local y UTC?

ToUniversalTime() convierte un valor local en UTC, y ToLocalTime() convierte un valor UTC de vuelta en hora local. Ambos devuelven un nuevo DateTime en lugar de modificar el original, ambos establecen el Kind del resultado y ambos aplican la regla de horario de verano que estaba vigente en el instante que se convierte.

Cada método comprueba primero Kind y no hace nada si el valor ya coincide. Llamar a ToLocalTime() sobre un valor que ya es Local lo devuelve tal cual, y por eso llamar dos veces a cualquiera de los dos métodos no causa ningún daño.

Unspecified es donde el asunto se vuelve delicado, porque los dos métodos discrepan a propósito sobre él. ToLocalTime() trata un valor sin indicador como si fuera UTC, mientras que ToUniversalTime() trata ese mismo valor como si fuera local. Pasa un mismo DateTime sin indicador a los dos y los resultados se mueven en direcciones opuestas, sin ninguna advertencia ni excepción.

var now = DateTime.Now;
var utcNow = DateTime.UtcNow;

if (utcNow.Kind == DateTimeKind.Utc)
{
    var oldKind = utcNow.Kind;
    var utcToLocal = utcNow.ToLocalTime();
    var newKind = utcToLocal.Kind;

    Console.WriteLine($"Converted {utcNow} from {oldKind} to {newKind}: {utcToLocal}");
}
if (now.Kind == DateTimeKind.Local)
{
    var oldKind = now.Kind;
    var localToUtc = now.ToUniversalTime();
    var newKind = localToUtc.Kind;

    Console.WriteLine($"Converted {now} from {oldKind} to {newKind}: {localToUtc}");
}

El primer bloque if convertirá el valor UTC al formato local y mostrará un mensaje que indica cómo se hizo la conversión.

The Web API Production Checklist, ebook gratuito en inglés

Ebook gratis

¿Tu Web API está lista para producción?

33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.

El ebook está en inglés.

Descarga la checklist gratis

PDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.

A la inversa, el segundo bloque if convertirá el valor local en uno universal, y el mensaje dará más detalles.

Esa división está documentada, no es un mito. La referencia de DateTime.ToUniversalTime de Microsoft da la regla para un valor sin indicador: “Se supone que el objeto DateTime actual es una hora local, y la conversión se realiza como si Kind fuera Local”.

Las comprobaciones con if de este ejemplo son las que lo mantienen correcto cuando los valores vienen de cualquier lugar distinto de Now y UtcNow. Sin ellas, ambos métodos convertirían un valor Unspecified, en direcciones opuestas, y la tabla siguiente detalla exactamente qué hace cada método con cada Kind.

Kind del valor de origenQué hace ToLocalTime()Qué hace ToUniversalTime()
UtcConvierte a hora localNada; devuelve el valor sin cambios
LocalNada; devuelve el valor sin cambiosConvierte a UTC
UnspecifiedSupone que el valor está en UTC y luego lo convierteSupone que el valor es local y luego lo convierte

Cualquiera de los dos métodos establece siempre el Kind de su resultado: ToLocalTime() devuelve Local y ToUniversalTime() devuelve Utc, así que es seguro llamar dos veces a cualquiera de ellos.

¿Cómo obtener la hora actual en código testeable?

DateTime.UtcNow es una propiedad estática que lee el reloj de la máquina, así que cualquier método que la llame directamente no se puede probar fácilmente en un momento elegido. Las pruebas que necesitan “dentro de una hora” acaban recurriendo a pausas o a comparaciones con un margen de tolerancia.

.NET 8 incorporó TimeProvider precisamente para esto. Es una clase abstracta con una instancia System que lee el reloj real y un método GetUtcNow() que devuelve un DateTimeOffset. El código recibe un TimeProvider como parámetro del constructor en lugar de llamar a DateTime.UtcNow. En producción se pasa TimeProvider.System, y una prueba pasa un fake ajustado al instante que necesite.

TimeProvider también abstrae los temporizadores y los retardos, algo importante para todo lo que consulta periódicamente, reintenta o caduca según una programación.

Si la plataforma de destino es anterior a .NET 8, no tenemos que escribir nuestra propia abstracción: el paquete Microsoft.Bcl.TimeProvider ofrece el mismo tipo a partir de .NET Framework 4.6.2 y .NET Standard 2.0, así que el patrón es idéntico en todas partes.

Lo tratamos en profundidad en el artículo sobre cómo probar código que depende del tiempo con TimeProvider en .NET, incluidos el proveedor fake y la compatibilidad con temporizadores.

Para el enfoque anterior a TimeProvider y la forma de una abstracción de reloj escrita a mano, consulta el artículo sobre cómo sobrescribir DateTime.Now durante las pruebas en .NET.

Conclusión

En este artículo, hemos aprendido a usar los formatos universal y local de DateTime, qué diferencia hay entre ellos y también cómo comprobar si un valor DateTime se creó en formato universal o en formato local.

Probado con .NET 10.0.10.