PatternRegex

Match an ISO 8601 timestamp

Pull full ISO timestamps out of logs and API payloads — date, T, time, optional milliseconds, and a required Z or offset so half-timestamps don't match.

Last updated

Patterng
\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})
Open in Regex Tester & Builder

Matches

  • 2026-09-04T18:30:00Z
  • 2026-09-04T18:30:04.250+05:30
  • 2026-09-04T02:00:00-08:00

Doesn’t match

  • 2026-09-04 18:30:00
  • 2026-09-04T18:30Z

Why it’s written this way

The shape is rigid on purpose: date, the literal T, a full HH:MM:SS, optional fractional seconds, then a REQUIRED zone — Z or an ±HH:MM offset. Requiring the zone is what separates a machine timestamp from a human "2026-09-04 18:30" that means whatever the writer's wall clock meant.

Edge cases to know

  • →Field ranges aren't checked (month 13 passes) — pair with the YYYY-MM-DD pattern's groups if you need that, or parse with a date library after extracting.
  • →Space-separated timestamps (common in Postgres logs) don't match — swap the T for [T ] to accept both.
  • →Compact ISO without separators (20260904T183000Z) is valid ISO 8601 but out of scope here.

Part by part

\d{4}-\d{2}-\d{2}
The date portion, in the year-month-day order that makes ISO timestamps sort correctly as plain text.
T
The literal separator between date and time. ISO 8601 permits a space in some contexts, but T is what machines emit and what this pattern requires.
\d{2}:\d{2}:\d{2}
Hours, minutes and seconds, each zero-padded to two digits.
(?:\.\d+)?
Optional fractional seconds. The digit count is left open because milliseconds, microseconds and nanoseconds all appear in the wild.
(?:Z|[+-]\d{2}:\d{2})
The zone, and it is required rather than optional: either Z for UTC or a signed offset. A timestamp with no zone is ambiguous, so this pattern refuses to match one.

Common mistakes

  • \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}

    Dropping the zone matches the string but throws away the only thing that makes it unambiguous. A timestamp without an offset is a local time in an unknown place, which is how a five-hour bug gets into a report.

    \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})
  • \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(?:Z|[+-]\d{2}:\d{2})

    Fixing the fraction at exactly three digits assumes milliseconds. Go and several databases emit nanoseconds, so this quietly fails to match timestamps from those systems while matching everything you tested with.

  • \d{4}-\d{2}-\d{2}.\d{2}:\d{2}:\d{2}(?:Z|[+-]\d{2}:\d{2})

    An unescaped dot in place of the T matches any character, so it accepts a timestamp separated by a hyphen, a slash or a letter — a looseness nobody intended and nobody notices until bad data arrives.

Questions

Does it accept a space instead of the T separator?

No. ISO 8601 allows a space by mutual agreement, and many databases display timestamps that way, but this pattern requires the T. Add an alternation of [T ] if you are parsing database output rather than machine-generated JSON.

Is +00:00 the same as Z?

As an offset, yes, they denote the same instant. As an intent they differ slightly: Z asserts UTC, while +00:00 can mean a local zone that happens to be at zero right now, such as the UK in winter. Both match here.

Does it validate that the date is real?

No. The date portion checks digit counts only, so 2026-13-45T00:00:00Z matches. Parse the result and confirm it survived the round trip if you need genuine validation rather than extraction.

Why require the offset when so many timestamps omit it?

Because an omitted offset is the single most common cause of time bugs, and a pattern that accepts it makes the problem invisible. Refusing to match is a useful signal that the data upstream is under-specified.

Related in Patterns