Registry Parser
All articles

Windows FILETIME timestamps, explained

8 min read

The Windows FILETIME is the timestamp you will read more often than any other in registry forensics, and it is the one most likely to be silently wrong if you convert it by hand. It is a 64-bit value, it is everywhere, and it does not use the epoch you assume it does. Learn to convert a FILETIME to a date correctly once and a large fraction of your timeline work stops generating off-by-centuries and off-by-hours mistakes. This post is the precise version of "what is this Windows timestamp and how do I read it."

What FILETIME actually is

A FILETIME is a count of 100-nanosecond intervals since 1601-01-01 00:00:00 UTC. That sentence has four parts and every one of them matters.

  • 100-nanosecond intervals. The unit is not seconds, not milliseconds, not microseconds. One tick is 10⁻⁷ seconds. There are 10,000,000 ticks in a second and 10,000 ticks in a millisecond. If your math is off by a factor of ten million, you divided by the wrong unit.
  • Since 1601-01-01. This is the gotcha that gets its own section below. The Unix epoch is 1970. FILETIME is 1601. They are 369 years apart.
  • UTC. A FILETIME is always UTC. It carries no timezone. To present it as a local wall-clock time you need the source system's offset, which is a separate read.
  • 64-bit. The value is a single unsigned 64-bit integer, conceptually the high and low 32-bit halves of a FILETIME struct concatenated. Sixty-four bits of 100-nanosecond ticks runs out somewhere around the year 60,000, so range is never your problem.

The 1601 epoch gotcha

The single most common FILETIME bug is treating it like a Unix timestamp. Unix time counts seconds since 1970-01-01 UTC. FILETIME counts 100-nanosecond ticks since 1601-01-01 UTC. Feed a FILETIME to a Unix-epoch converter and you do not get a date that is a little wrong — you get a date roughly four hundred years into the future, because you have both the wrong unit and the wrong origin.

Why 1601? It is the start of the 400-year Gregorian calendar cycle that contained the date Windows NT was designed in, which makes leap-year arithmetic clean. You do not need to care about the reason. You need to care that the offset between the two epochs is a fixed constant:

116444736000000000 ticks  =  number of 100-ns intervals between
                             1601-01-01 and 1970-01-01 (UTC)

That magic number is the bridge between the two worlds, and it is the heart of every correct conversion.

How to convert a FILETIME to a date

The arithmetic is mechanical:

unix_seconds = (filetime - 116444736000000000) / 10000000

Subtract the epoch offset to rebase from 1601 to 1970, then divide by ten million to turn ticks into seconds. Feed unix_seconds to any standard date library, treat the result as UTC, and you are done. The remainder of that division (the sub-second part) is your fractional seconds if you need them; multiply the remainder back out by 100 to recover nanoseconds.

If you would rather skip the rebase, convert directly: the total seconds since 1601 is filetime / 10000000, and you add that many seconds to the 1601-01-01 origin. Same answer, one fewer constant to memorize, but most date libraries speak Unix time so the rebase is usually the path of least resistance.

A concrete worked conversion

Take the 64-bit value 0x01DCB8A2C4E10000. Read it as a decimal integer:

0x01DCB8A2C4E10000  =  134066304000000000  ticks

Rebase to the Unix epoch by subtracting the constant:

134066304000000000 - 116444736000000000  =  17621568000000000  ticks since 1970

Divide by ten million to get seconds:

17621568000000000 / 10000000  =  1762156800  seconds since 1970-01-01 UTC

Hand 1762156800 to a date library as Unix seconds and it resolves to:

2025-11-03 08:00:00 UTC

That is the whole procedure. Notice that the answer is UTC and stays UTC; nothing in those four lines knows or cares what timezone the host was in.

Little-endian, and what you see in raw hex

This is where the value confuses people reading hives by hand. On disk and in the registry, a FILETIME is stored little-endian across 8 bytes — least significant byte first. So the value 0x01DCB8A2C4E10000 from the example above does not appear in that order in a hex dump. It appears byte-reversed:

on-disk bytes:   00 00 E1 C4 A2 B8 DC 01
logical value:   0x01 DC B8 A2 C4 E1 00 00

To reconstruct the integer you read the eight bytes and reverse them. Get the byte order wrong and the value is garbage — usually a date in the wrong millennium, which is at least an obvious-enough error that you catch it. The subtler trap is grabbing the wrong eight bytes: a value blob often packs the FILETIME alongside other fields, so you must know the offset, not just the length. The UserAssist record, for instance, puts its last-run FILETIME at offset 0x3C of a 72-byte structure, not at the start.

Where FILETIME shows up

Once you can read it, you will see FILETIME nearly everywhere a registry artifact records "when":

  • Key LastWrite time. Every registry key carries a FILETIME in its key node (nk) recording when the key was last modified. This is metadata, not a value, and it is the basis for nearly all registry timelining — see registry LastWrite time for the full treatment of what it does and does not tell you.
  • UserAssist last-run. The 8-byte last-run timestamp inside each UserAssist Count value is a FILETIME, at a fixed offset within the binary record.
  • BAM / DAM. Background Activity Moderator execution times under SYSTEM\CurrentControlSet\Services\bam\State\UserSettings\ are FILETIME blobs, one per executable, recording last execution.
  • Countless value blobs. USBSTOR device install and first/last-connect times, AppCompatCache entries, shellbag MRU timestamps, and a long tail of REG_BINARY structures embed FILETIME fields. Whenever you find an 8-byte field in a blob that decodes to a plausible recent date, suspect FILETIME first.

The SYSTEMTIME contrast

FILETIME is not the only timestamp format in the registry, and the other common one behaves completely differently. SYSTEMTIME is a 16-byte structure of discrete fields — separate 16-bit integers for year, month, day-of-week, day, hour, minute, second, and milliseconds. It is human-shaped: you read the year out of bytes 0–1, the month out of bytes 2–3, and so on, with no arithmetic and no epoch.

That convenience hides a forensic catch. SYSTEMTIME has no timezone field, and the way Windows fills it in, the wall-clock values are typically local time, not UTC. The canonical registry example is the network history: NetworkList uses SYSTEMTIME for its DateCreated and DateLastConnected values, and those are local-time wall-clock readings. To put a SYSTEMTIME on a UTC timeline you must apply the system's bias yourself.

So the two formats invert each other's strengths. FILETIME is a compact integer that is always UTC and needs arithmetic to read; SYSTEMTIME is a fat struct that reads at a glance but is local time and needs the timezone to anchor. Misidentify which one you are looking at — eight bytes of little-endian ticks versus sixteen bytes of discrete fields — and you will either apply a bias to a value that is already UTC or present a local time as if it were UTC.

FILETIME is UTC, but you still need the timezone

It is tempting to conclude that because FILETIME is always UTC, the timezone never matters for it. That is true for the conversion and false for the report. The conversion of a FILETIME to an absolute instant needs no timezone at all — the math above produces a UTC datetime from nothing but the eight bytes. But the people reading your timeline think in local time, and the events around the FILETIME — the SYSTEMTIME artifacts, the Event Log entries rendered by a tool, the user's own account of their day — are anchored to the host's wall clock.

So you still establish the system time zone for context, even when every FILETIME in the case is UTC. Convert the FILETIME to UTC by the arithmetic, then, if you choose to also display local time, apply the bias as a clearly labeled second column. The discipline is to keep UTC as the source of truth and treat local time as a derived, annotated convenience — never the other way around.

The habit

Read FILETIME as eight little-endian bytes, reverse them to an integer, subtract 116444736000000000, divide by 10000000, and interpret the result as UTC. Sanity-check the answer against the year you expect; a date in the 1600s means you forgot to subtract the epoch, and a date in the 2400s means you forgot the byte order. When the blob is a discrete-field struct instead of an integer, you are holding a SYSTEMTIME, and the rules flip to local time. You can analyze a hive in your browser and have FILETIME values across LastWrite, UserAssist, BAM, and the value blobs decoded to UTC for you — but knowing the four-line conversion is what lets you check the tool instead of trusting it.