Skip to main content
Productivity

Your logs are full of UTC numbers. Here's the fix.

Your log file has 1727770000 in it three lines up from an error. Is that a minute before the bug? An hour? Last Tuesday? You either squint at the number and guess, or you open a tab and type it into a converter. There is a faster route, but the interesting part is why this number ends up in your logs at all and what it quietly costs.

Why time() shows up everywhere

Databases store it, session providers store it, cache backends store it, and half of your JSON APIs return it to your users instead of a real date. It's a simple integer of seconds since 1970, so you can add, subtract, and sort without any formatting library. It's a genuinely good idea. The problems come at the edges of the system.

The edge where the user's timezone shows up: you render expires_at raw, and every user in a negative offset thinks your session expired hours ago. The edge where the system clock shows up: you hardcode time() + 86400 for "one day" and then daylight saving shifts everyone an hour, or you forget that 86400 is not a day twice a year in half the world's timezones. The edge where your own confidence shows up: you read one timestamp, get the day right but the hour wrong, and decide the log line is unrelated to the bug you're hunting. That last one is expensive because it doesn't fail. You just debug in the wrong direction.

Writing timestamps without breaking them

Three habits, none of them complicated:

  • When you need "now plus a while", use now()->addMinutes(30) or a Carbon helper rather than time() + 1800. Carbon knows DST.
  • When you store a literal column name like expires_at, keep it a real datetime column (or an integer if your framework demands it) and be consistent from table to table. Mixed formats across tables is where bugs breed.
  • When you parse an incoming timestamp, parse it in UTC and convert to local only at the display layer. If you parse in local time and display in UTC too, you silently shift every historical date by your current offset.

If you want to sanity check one by hand, paste it into the unix timestamp converter. It shows the date in your timezone and in UTC side by side, which is exactly how you catch the "it's 22:00 UTC, so locally that's actually tomorrow" mistake before it bites you at 23:59 on a cron job.

Reading them back without guessing

The whole reason this is worth a blog post is that timestamps pile up in places nobody formats for you: log aggregators, third-party API responses, database dumps someone sends you. The useful move is to make "which date is this?" a one-second answer instead of a lookup, because you'll do it ten times a day when something is broken.

If you're writing your own converter for a dashboard, don't just format for one timezone. Intl.DateTimeFormat() in the browser takes a timeZone option and will render the same value correctly for any user, which is cheaper than asking every user for their timezone and storing it. Timezone Converter does the same on the server, which is more useful for batch jobs and reports than for a single page.

Six days of debugging, one line

A friend of mine spent a week chasing a "the API is slow at night" complaint. Logs were timestamped in UTC, the user was in UTC-5, and the slow period was 3am UTC. He was reading log timestamps as local times, correlating against nothing, and concluding the API was slow at 3am local. The fix was one timezone flag on his log parser. One flag, five days.

If timestamps are your week's problem today, the timestamp converter will not fix your code. But it will turn "is 1727770000 today or yesterday?" from a guess you act on into a fact you check, and that's usually where debugging starts.