.NET (C#, F#, VB.NET)
In brief: .NET's own date types are not affected by 2038. They count time in 100-nanosecond "ticks" in a 64-bit number and run to the end of year 9999. The risks are where .NET meets other systems: Unix timestamps squeezed into a 32-bit
int, native libraries on 32-bit Linux, and older SQL Server and COM interfaces. Since .NET 9, the official 32-bit Arm builds for Linux are built for 64-bit time.
| Risk level | Low for the runtime. Medium for interop code and older Arm32 deployments |
| Status | DateTime/DateTimeOffset are safe to 9999. The Arm32 Linux runtime has been built against a Y2038-compatible glibc since .NET 9. Application-level int conversions remain |
| Last reviewed | 2026-10-21 |
2038 status at a glance
- At risk Confirmed.NET (int) casts and int columns holding epoch secondsApplication-level truncation to Int32 overflows in 2038.Do: Use long or DateTimeOffset end to end.
- At risk ConfirmedSQL Server 2017–2022 DATEADD(second, epoch, '1970-01-01')DATEADD takes an int argument, so epoch seconds past 2038 overflow. SQL Server 2025 and Azure accept bigint.Do: Upgrade to SQL Server 2025 or Azure SQL (bigint argument), or convert epoch values in application code.
Not affected or different deadline (1), and why
- Not affected.NET DateTime, DateTimeOffset and ToUnixTimeSecondsThese use 64-bit 100-ns ticks from year 1 to 9999. Unix-time helpers return Int64.
Overview
System.DateTime stores a 64-bit Ticks value: "the number of 100-nanosecond intervals that have elapsed since 12:00:00 midnight, January 1, 0001" [1]. DateTime.MaxValue is 23:59:59.9999999 on 31 December 9999, which is 3,155,378,975,999,999,999 ticks [2]. DateTimeOffset adds a UTC offset to the same tick count. Managed code that stays in these types has no 2038 boundary. It does have a year-10000 boundary, which is a different problem.
The Unix-time helpers are 64-bit as well:
DateTimeOffset.ToUnixTimeSeconds()returnslong(Int64). It exists since .NET Framework 4.6 and .NET Core 1.0 [3].DateTimeOffset.FromUnixTimeSeconds(long)accepts −62,135,596,800 to 253,402,300,799. Values outside that range throwArgumentOutOfRangeException[4].
Technical details
Pitfall 1: storing Unix seconds in int
// BUG: explicit cast truncates silently in the default unchecked context
int expires = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds() + ttl;
// BUG: old hand-rolled helpers often look like this
static DateTime FromUnix(int secs) => new DateTime(1970,1,1,0,0,0,DateTimeKind.Utc).AddSeconds(secs);
// FIX: long end to end
long expires = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + ttl;
static DateTimeOffset FromUnix(long secs) => DateTimeOffset.FromUnixTimeSeconds(secs);
// FIX at a boundary you can't change: fail loudly
int wire = checked((int)secs); // OverflowException instead of wrapping
C# compiles in an unchecked context by default, so (int)someLong keeps the low 32 bits without any error. Turn on <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow> in test builds, or use checked(...) at serialisation boundaries.
Pitfall 2: P/Invoke and time_t
There is no time_t type in .NET. Interop signatures are hand-written, and many older ones declare the native time value as int:
// BUG: assumes 32-bit time_t; wrong once the native side is 64-bit
[DllImport("libc")] static extern int time(IntPtr t);
// FIX: on 64-bit Linux/macOS time_t is 64-bit (long in C#). On 32-bit
// Linux it depends on how the native library was built, so prefer
// managed APIs (DateTimeOffset.UtcNow) over calling libc directly.
[DllImport("libc")] static extern long time(IntPtr t); // 64-bit targets
Structs marshalled with [StructLayout(LayoutKind.Sequential)] that mirror struct timespec or struct stat also change layout when the native time_t grows. See Lang-C-and-CPP.
The runtime itself on 32-bit Arm Linux
The runtime's native shim (System.Native, the crypto shim and others) is C code, so it inherits the C library's time_t. On Ubuntu 24.04 armhf, which moved to 64-bit time_t, HTTPS requests from older .NET builds failed with a NotTimeValid certificate error (dotnet/runtime #101444). PR #102410, merged 2024-05-22, rebuilt the linux-arm runtime against a glibc with _TIME_BITS=64. It also added run-time detection of whether the system OpenSSL uses 32-bit or 64-bit time_t, so that it doesn't pass values that don't fit [5]. The .NET 9 supported-OS notes now say: "Microsoft-provided portable Arm32 glibc builds are supported on distro versions with a Y2038 compatible glibc, for example Debian 12, Ubuntu 22.04, and higher versions" [6].
A related dotnet/docs pull request (#40669) drafted breaking-change notices saying that .NET 6, 8 and 9 are "not supported on Y2038-compatible Arm32 distros". That PR shows as closed, not merged, so treat its exact version wording as (unverified). Check the current supported-OS matrix for your .NET release before you deploy on armhf [7].
Windows CRT underneath
Native code that .NET calls on Windows uses the UCRT, where time_t is 64-bit by default and _USE_32BIT_TIME_T is discouraged. Microsoft warns that "your application may fail after January 18, 2038" if you define it [8]. Old native DLLs built with that macro, or with Visual C++ 2003 or earlier, may still return 32-bit times.
COM / OLE Automation DATE
The OLE Automation DATE (VT_DATE) is an 8-byte double. Its integer part counts days from 30 December 1899 and its fraction is the time of day [9]. DateTime.ToOADate() covers 1 January 0100 to 31 December 9999 and throws OverflowException outside that range [10]. It has no 2038 limit, but it has other hazards:
- Values before 1899-12-30 are discontinuous because the fraction is always added. A plain floating-point comparison sorts some pre-1900 times incorrectly [9].
DATEis local time with no time zone or DST information [9].- Precision gets coarser as the magnitude grows, because the value is a double.
Excel, VBA, Access and classic ASP all use this representation. The risk there is a 32-bit Unix timestamp column that someone has bolted on beside it, not DATE itself.
Win32 FILETIME
FILETIME is a 64-bit count of 100 ns intervals since 1601-01-01, and it's safe. Interop code that splits it into dwLowDateTime/dwHighDateTime and then converts to Unix seconds in an int brings the 2038 bug back. See OS-Windows.
SQL Server from .NET
A common T-SQL idiom for converting epoch seconds is DATEADD(second, @unix, '1970-01-01'). On SQL Server 2017–2022 the number argument is an int. Values beyond 2,147,483,647 fail with Msg 8115 Arithmetic overflow error converting expression to data type int. SQL Server 2025 (ver17), Azure SQL and Fabric document number as bigint [11]. On older versions, split the value: add days first, then the remaining seconds. See Databases.
Affected versions / products
| Item | Representation | Range / 2038 exposure |
|---|---|---|
DateTime, DateTimeOffset |
Int64 ticks (100 ns) since 0001-01-01 | to 9999-12-31; none |
ToUnixTimeSeconds / FromUnixTimeSeconds |
Int64 | none (.NET Framework 4.6+, .NET Core 1.0+) |
OLE DATE / ToOADate |
double days since 1899-12-30 | 0100–9999; none |
App code (int) casts, int columns |
Int32 | 2038 |
| .NET on linux-arm (32-bit glibc) | native time_t |
Built Y2038-compatible since .NET 9 [5][6]. Older builds on t64 distros (see above) |
SQL Server 2017–2022 DATEADD(second, …) |
int argument | 2038 for epoch conversion |
Risk & compliance notes
- Inventory P/Invoke signatures (
DllImport/LibraryImport) that mention time,intcolumns in EF Core models that hold epoch seconds, and JSON contracts withinttimestamps. - Arm32 Linux IoT deployments running .NET 8 or earlier on a t64 distribution are the most likely to fail before 2038, through certificate validation.
- Owners: application developers (casts, contracts), DBAs (T-SQL helpers), and platform teams (Arm32 runtime versions).
Detection & mitigation
- Search for
(int)next toToUnixTimeSeconds,TotalSecondsandTicks /, and forAddSeconds(withintparameters. - Turn on checked arithmetic in CI test runs (
CheckForOverflowUnderflow) so that wraps throw. - Inject
TimeProvider(.NET 8+) and test with2038-01-19T03:14:08Zand later. - Move linux-arm workloads to .NET 9 or later on a Y2038-compatible distro, or to arm64.
- Follow Testing-and-Detection and Remediation-Guide.
See also
References
- DateTime.Ticks Property — Microsoft Learn — 100 ns ticks since 0001-01-01; Int64.
- DateTime.MaxValue Field — Microsoft Learn — 9999-12-31 23:59:59.9999999; 3,155,378,975,999,999,999 ticks.
- DateTimeOffset.ToUnixTimeSeconds Method — Microsoft Learn — returns Int64; available from .NET Framework 4.6 / .NET Core 1.0.
- DateTimeOffset.FromUnixTimeSeconds(Int64) — Microsoft Learn — valid range and
ArgumentOutOfRangeException. - Build against Y2038-compatible glibc for linux arm32 — dotnet/runtime PR #102410, merged 2024-05-22 — fixes #101444 (
NotTimeValidon Ubuntu 24.04 Arm32); OpenSSLtime_tdetection. - .NET 9 supported OS — dotnet/core release notes — Arm32 builds require Y2038-compatible glibc (Debian 12, Ubuntu 22.04+).
- Add Y2038 related breaking change notices — dotnet/docs PR #40669 — draft notices for .NET 6/8/9 on Y2038-compatible Arm32 distros; PR closed.
- time, _time32, _time64 — Microsoft Learn — 64-bit
time_tdefault;_USE_32BIT_TIME_Twarning. - DATE Type — Microsoft Learn (ATL/MFC) — 8-byte float, days since 1899-12-30, discontinuity before 0.
- DateTime.ToOADate Method — Microsoft Learn — OLE date range 0100–9999;
OverflowException. - DATEADD (Transact-SQL) — Microsoft Learn — number is int on SQL Server 2017–2022, bigint on SQL Server 2025/Azure; Msg 8115.