← All posts
DEV

Whose midnight counts? Building a timezone-aware leaderboard

Summary

To reset a leaderboard at each user's local midnight regardless of their timezone, don't wipe your database. Instead, create 'date-based buckets' and serve them based on the client's local date, validated within a ±24-hour UTC window.

Ever use Duolingo?

Dua lipa (it's a joke, It's duolingo)
Dua lipa (it's a joke, It's duolingo) Source: https://www.uvureview.com/valley-life/kidnapping-bird-gone-viral-duolingo/

I do every day. One day I flew to NYC - completely different timezone - and my streak was still intact. My league ranking hadn’t moved. Nothing reset.

I kept thinking about it. How?

Then I ran into the exact same problem building the daily leaderboard for Glowtris. It resets at midnight. But whose midnight? Seoul’s? New York’s? UTC?

The answer is - Yours. Wherever you are.

The obvious approach (that doesn’t work)

The simplest solution is a cron job that wipes the leaderboard at UTC midnight every day.

I literally wrote this:

// ❌ DON'T DO THIS
const wipeLeaderboard = async () => {
  await redis.del('glowtris-daily-leaderboard')
}

// Run at UTC midnight
schedule.scheduleJob('0 0 * * *', wipeLeaderboard)

But that means Seoul players see a reset at 9am. New York players see one at 7pm the day before. Nobody gets an actual midnight reset. It just resets at some arbitrary UTC moment that means nothing to anyone.

What about running a cron per timezone? That’s 38 IANA timezones, each needing its own leaderboard. Possible, but messy - and suddenly your Seoul board has no New York players on it.

There’s a cleaner way.

Data Buckets

Instead of ‘wiping’ the leaderboard, just make a new one every day.

glowtris-daily-2026-06-05  ← today for Seoul
glowtris-daily-2026-06-06  ← today for New York

When you load the leaderboard, your device sends your local date.

const localDate = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`

fetch(`/api/leaderboard?date=${localDate}`)

The server returns whichever bucket matches. When midnight hits for you, your local date changes — and you see a fresh, empty board. The old one isn’t deleted. It just stops being shown.

“Can’t you just send a fake date?”

Yes, technically. You could send yesterday’s date and post to an old board.

So the server checks: the maximum legitimate timezone offset is UTC+14 (parts of Kiribati). That means any date more than one day away from UTC today gets rejected. Every real timezone passes. Anything suspicious doesn’t.

How long to keep the data

Redis keys need to expire eventually. I originally set 26 hours — fine for UTC-only, not enough for timezone-aware dates.

A key for 2026-06-06 might first be written by a UTC+14 user at 10:00 UTC on June 5th, and last written by a UTC-12 user at 12:00 UTC on June 8th. That’s about 50 hours apart. So I set TTL to 52 hours — enough for everyone’s day to finish, then it quietly expires.


That’s it

The server doesn't force its own timezone on you. It simply trusts the local date your device provides.

You play in Seoul just before midnight. Fly to New York. Fourteen hours pass locally, and you are back in yesterday. Since your device sends yesterday's date, you continue competing on yesterday's leaderboard without losing your streak.

The server doesn’t care about its own clock. It makes sure midnight is always midnight for you.

Play Now

GLOWTRIS

Free neon block puzzle. Play instantly in your browser!

🎮 Play Free →
glowtris.com

Keep reading