Explainer

Unix timestamps in milliseconds: convert, compare and avoid time-zone bugs

Thirteen digits, no time zone, no ambiguity. Almost every timestamp bug happens after the parse: in the unit you assumed, the zone you forgot, or the event you thought it marked.

On this page
  1. Seconds, milliseconds, microseconds and nanoseconds
  2. Convert a Unix timestamp in Python, JavaScript, SQL and the shell
  3. UTC to EST and EDT without hard-coded offsets
  4. Six timestamp bugs that reach production
  5. Which event does a market data timestamp mark?
  6. Measure freshness with one subtraction
  7. Questions

Key takeaways

  • A Unix timestamp in milliseconds counts milliseconds since 00:00:00 UTC on 1 January 1970; current values have 13 digits.
  • Count the digits before converting: 10 means seconds, 13 milliseconds, 16 microseconds and 19 nanoseconds.
  • Unix time has no time zone. Convert to UTC explicitly, then to a named zone such as America/New_York, never to a fixed offset.
  • A daily market bar is stamped at 00:00 UTC of its session date; shifting it into New York time moves it to the previous day.
  • The age of a price is your clock minus its timestamp, and it only means something next to the market's session state.

A Unix timestamp in milliseconds is the number of milliseconds elapsed since the Unix epoch, 00:00:00 UTC on 1 January 1970, not counting leap seconds. 1790591112968 is 2026-09-28 10:25:12.968 UTC. Divide by 1,000 to get seconds. In JavaScript, new Date(1790591112968) reads it directly; in Python, use datetime.fromtimestamp(ms / 1000, tz=timezone.utc).

Market data runs on these numbers: every price from a market data API such as TickerLayer carries one. Paste any value into the converter below to see it in UTC, New York time and your own zone, then read on for conversions in code and the bugs that come with them.

Unix timestamp converter

Read as
milliseconds
ISO 8601 (UTC)
2026-09-28T10:26:43.836Z
New York
…
Your time
…
Milliseconds
1790591203836
Seconds
1790591203

Seconds, milliseconds, microseconds and nanoseconds are detected by length.

Paste a Unix timestamp in seconds or milliseconds. The conversion runs in your browser.

Seconds, milliseconds, microseconds and nanoseconds

One instant can be written at four precisions, and the digit count gives the unit away. For dates between 2001 and 2286, seconds have 10 digits and every finer unit adds three more. Epoch time in milliseconds, the 13-digit form, is what JavaScript and most market data APIs use.

UnitDigitsThe same instantCommon in
Seconds101790591112Unix tools, token expiry fields, many databases
Milliseconds131790591112968JavaScript Date.now(), most market data APIs, TickerLayer
Microseconds161790591112968000Database timestamp types, some trade feeds
Nanoseconds191790591112968000000Exchange-grade feeds, Python time.time_ns()
10:25:12.968 UTC on 28 September 2026 at four precisions.

When you accept timestamps from more than one source, normalize by magnitude at the boundary, before anything else touches the value:

normalize.pyPython
def to_ms(t: int) -> int:
    """Normalise a Unix timestamp in s, ms, µs or ns to milliseconds, by magnitude."""
    if t < 10**11:  # seconds (a millisecond value this small would be before March 1973)
        return t * 1000
    if t < 10**14:  # milliseconds
        return t
    if t < 10**17:  # microseconds
        return t // 1000
    return t // 1_000_000  # nanoseconds


for t in (1790591112, 1790591112968, 1790591112968000, 1790591112968000000):
    print(f"{t:>19} -> {to_ms(t)}")
Output
         1790591112 -> 1790591112000
      1790591112968 -> 1790591112968
   1790591112968000 -> 1790591112968
1790591112968000000 -> 1790591112968

Convert a Unix timestamp in Python, JavaScript, SQL and the shell

Every language can convert a Unix timestamp; the differences are in the unit each function expects and the zone it assumes. Python's fromtimestamp takes seconds and, without tz=, silently uses the machine's local zone. The last line below shows that bug on a machine set to UTC+3:

convert.pyPython
from datetime import datetime, timezone
from zoneinfo import ZoneInfo  # Python 3.9+

ms = 1790591112968  # "timestamp" from GET /crypto/quote/BTCUSD

utc = datetime.fromtimestamp(ms / 1000, tz=timezone.utc)
print(utc.isoformat(timespec="milliseconds"))
print(utc.astimezone(ZoneInfo("America/New_York")))
print(round(utc.timestamp() * 1000))      # back to milliseconds
print(datetime.fromtimestamp(ms / 1000))  # no tz: the machine's local zone, a bug
Output
2026-09-28T10:25:12.968+00:00
2026-09-28 06:25:12.968000-04:00
1790591112968
2026-09-28 13:25:12.968000

JavaScript's Date stores milliseconds natively, so no division is needed. Format with an explicit timeZone rather than relying on the zone of the browser or server:

convert.mjsJavaScript
const ms = 1790591112968; // "timestamp" from GET /crypto/quote/BTCUSD

const d = new Date(ms); // takes milliseconds directly
console.log(d.toISOString());
console.log(d.toLocaleString("en-US", { timeZone: "America/New_York", timeZoneName: "short" }));
console.log(Math.floor(ms / 1000)); // to seconds

// Nanoseconds do not fit in a JavaScript number
console.log(Number("1790591112968123456"), BigInt("1790591112968123456"));
Output
2026-09-28T10:25:12.968Z
9/28/2026, 6:25:12 AM EDT
1790591112
1790591112968123400 1790591112968123456n
SQL and shell
-- PostgreSQL (session time zone UTC)
SELECT to_timestamp(1790591112968 / 1000.0);            -- 2026-09-28 10:25:12.968+00
SELECT (extract(epoch FROM now()) * 1000)::bigint;       -- now, in milliseconds

-- SQLite
SELECT strftime('%Y-%m-%dT%H:%M:%fZ', 1790591112968 / 1000.0, 'unixepoch');
-- 2026-09-28T10:25:12.968Z

# Shell: date takes seconds
date -u -d @1790591112   # GNU date, Linux
date -u -r 1790591112    # BSD date, macOS

In pandas, pd.to_datetime(df["t"], unit="ms", utc=True) converts a whole column of bar timestamps at once. The utc=True is not decoration, as the next section shows.

UTC to EST and EDT without hard-coded offsets

A Unix timestamp has no time zone. It names one instant everywhere on Earth, and a zone appears only when you format it for a person. Bugs start when code formats in the server's zone by accident, or converts with a fixed offset.

New York is the usual target, and it has two offsets. Eastern Standard Time (EST) is UTC-5, from the first Sunday of November to the second Sunday of March; Eastern Daylight Time (EDT) is UTC-4 for the rest of the year. So "UTC to EST" is only right in winter: in late September, 10:25 UTC is 06:25 EDT, not 05:25. The US stock session shows the shift on a UTC clock:

The US stock session on a UTC clock

Summer (EDT)UTC-4, March to NovemberPreRegularAfter
Winter (EST)UTC-5, November to MarchPreRegular

Hours in UTC

The regular session is 09:30 to 16:00 New York time all year, with pre-market from 04:00 and after-hours until 20:00. In UTC, all of it moves by an hour twice a year.

Two more traps hide in the calendar. The US and Europe change their clocks on different dates: in 2026 New York moves on 8 March and 1 November, London on 29 March and 25 October. For those weeks the gap between the two cities is four hours instead of five, and any schedule built on a fixed offset is an hour off. Convert with a real zone database (America/New_York, never -05:00) and let it carry the rules. Our US market hours page shows the current session and holidays in local time.

Six timestamp bugs that reach production

  • Seconds read as millisecondsnew Date(1790591112) is 21 January 1970. A date in 1970 almost always means a seconds value went into a milliseconds API.
  • Milliseconds read as secondsMultiply 13 digits by 1,000 again and you land in the year 58711. Normalize by digit count at the boundary.
  • Naive local timePython datetime.fromtimestamp(ms / 1000) without tz= uses the machine's zone. At UTC+3 it prints 13:25 for a 10:25 UTC trade.
  • Hard-coded offsetsMinus five hours is right for New York only in winter. Use America/New_York and let the zone database handle daylight saving time.
  • Precision lossJavaScript numbers are exact only up to 2^53, about 9 × 10^15. Milliseconds fit; nanoseconds do not, so parse those as BigInt or strings.
  • The 2038 overflowA signed 32-bit seconds counter runs out at 03:14:07 UTC on 19 January 2038. Store milliseconds in 64-bit integers.

Which event does a market data timestamp mark?

Parsing is solved. The harder question in market data is what a timestamp describes, because a price passes through several clocks on its way to you:

MarketTickerLayerYour app
  1. a trade printsevent time
  2. the update reaches the aggregation layerreceipt timeMarket to TickerLayer
  3. response or frame sentsend timeTickerLayer to Your app
  4. your code reads ityour receipt time
Each hop adds time. The distance between the first clock and the last is how old the price is when you act on it.

On TickerLayer, REST quotes and trades carry timestamp, a snapshot carries last_timestamp for its last trade, and stream frames carry ts. A snapshot frame replayed when you subscribe keeps its original ts, so outside trading hours it can be hours older than the moment it arrives. Bars are the special case: their t marks where the bar starts.

FieldFound inMarksExample
timestampREST quote and last tradeThe time of the quote or trade1790591112968
last_timestampREST snapshotThe last trade, which may belong to the previous session1790589160160
tREST barsBar start; daily bars at 00:00 UTC of the session date1790294400000
tsWebSocket framesThe update, unchanged on snapshot replays1790591203836
bar_start, bar_end, as_ofPrevious bar with ?interval=The bar window, and when the answer was produced1790366340000
next_open, local_timeMarket statusISO-8601 strings, in UTC and in the market's local offset2026-09-28T13:30:00.000Z
Time fields in TickerLayer responses. Every integer is Unix milliseconds, UTC.

The daily bar row surprises people. The US:KO bar for Friday 25 September 2026 carries t = 1790294400000, which is 00:00 UTC that day, although trading ran from 13:30 to 20:00 UTC. The stamp names the session, not the closing bell, and it is why you should never shift a daily bar into New York time: 00:00 UTC becomes 20:00 the evening before, and every bar lands on the wrong date. OHLC and OHLCV explained covers bar conventions, and historical stock data covers sessions and closes.

A snapshot frame, replayed on subscribe

{
  "type": "quote",
  "channel": "stocks.quotes",
  "asset": "stocks",
  "symbol": "US:KO",
  "bid": "88.11",
  "ask": "88.3",
  "bid_size": "400",
  "ask_size": "200",
  "ts": 1790590887491,1
  "snapshot": true2
}
  1. ts10:21:27.491 UTC: the time of the quote, older than the moment the frame was sent.
  2. snapshotMarks a replayed last-known value rather than a new tick. Live ticks omit the field.
  3. bid / askStock prices on the stream arrive as strings; convert them before doing arithmetic.
stocks.quotes frame for US:KO, captured in the US pre-market on 28 September 2026 with snapshot set to always.

Every frame type and its time field is listed in the WebSocket message reference.

Measure freshness with one subtraction

Once you know what a timestamp marks, a price's age is one line: your clock now minus the timestamp. Freshness checks in alerting and trading code are built on exactly this:

age.pyPython
import os
import time
from datetime import datetime, timezone
from zoneinfo import ZoneInfo  # Python 3.9+

import requests

NEW_YORK = ZoneInfo("America/New_York")

resp = requests.get(
    "https://api.tickerlayer.com/stocks/snapshot/US:KO",
    headers={"x-api-key": os.environ["TICKERLAYER_API_KEY"]},
    timeout=10,
)
resp.raise_for_status()
snap = resp.json()

ms = snap["last_timestamp"]                      # Unix milliseconds, UTC
utc = datetime.fromtimestamp(ms / 1000, tz=timezone.utc)
age_s = (time.time_ns() // 1_000_000 - ms) / 1000

print("last trade  ", snap["last_price"])
print("UTC         ", utc.isoformat(timespec="milliseconds"))
print("New York    ", utc.astimezone(NEW_YORK).isoformat(timespec="milliseconds"))
print(f"age          {age_s:,.1f} s")
Output (28 September 2026, 10:56 UTC)
last trade   88.16
UTC          2026-09-28T10:50:28.984+00:00
New York     2026-09-28T06:50:28.984-04:00
age          346.8 s

Almost six minutes is not a fault here. It was 06:56 in New York, pre-market, and the stock had simply not traded since. Age only means something next to the market's state, which is why a freshness rule needs a session calendar and a threshold per asset class; the bad ticks guide builds those rules.

Questions

How do I convert a Unix timestamp in milliseconds to a date?

In JavaScript, pass it to new Date(ms), which takes milliseconds directly. In Python, divide by 1,000 and set the zone explicitly: datetime.fromtimestamp(ms / 1000, tz=timezone.utc).

How many digits is a Unix timestamp in milliseconds?

Thirteen, for any date between September 2001 and November 2286. A 10-digit value is in seconds.

Is a Unix timestamp always UTC?

Yes. It counts from midnight UTC on 1 January 1970 and carries no time zone of its own; a zone is applied only when you format it for display.

How do I convert epoch time to EST?

Convert to UTC first, then to the America/New_York zone, which applies EST (UTC-5) in winter and EDT (UTC-4) in summer. Subtracting a fixed five hours is wrong for most of the year.

Why does my timestamp show a date in 1970?

You passed seconds to a function that expects milliseconds, so the value reads as a few weeks after the epoch. Multiply by 1,000, or check the digit count first.

How do I get the current Unix timestamp in milliseconds?

In JavaScript, Date.now(). In Python, time.time_ns() // 1_000_000. In a Linux shell, date +%s%3N with GNU date.

Keep reading

Ready to integrate?

Start with the free tier, explore the docs, and connect via REST or WebSocket in minutes.