DEV Community

Cover image for Adding a self-destructing timer to a disposable email service
15 Minute Mail
15 Minute Mail

Posted on

Adding a self-destructing timer to a disposable email service

I run 15minutemail.com. The pitch is simple: you get an email inbox that lasts 15 minutes. When time's up, everything gets deleted. But if you need more time, you can extend it.

Sounds trivial. It mostly was, except for the parts that weren't.

The timer isn't just a countdown

The obvious approach is to start a setTimeout on the client and show a countdown. That works for display purposes but it doesn't actually control anything. The real TTL lives on the server.

When a user opens the site, the backend creates a session with an expires_at timestamp set to now + 15 minutes. This timestamp is stored in SQLite alongside the temporary email address. The client gets the expiration time in the API response and renders a countdown from it.

If the user closes the tab and comes back, the timer picks up where it left off because it's based on the server timestamp, not a client-side counter. No cheating by refreshing the page.

The cleanup job

A background job runs every 60 seconds. It queries SQLite for all sessions where expires_at < now, deletes the associated emails, and removes the session. In the SMTP layer (Haraka), incoming emails for expired addresses get rejected at the protocol level, so they never even hit storage.

I thought about using Redis TTL for automatic expiration but I wanted the emails in SQLite for the duration of the session so users could reload without losing messages. Redis TTL would delete the data out from under them. SQLite with a manual cleanup job is less elegant but more predictable.

The extend button

This was the feature that set the project apart from 10-minute-mail clones. Most disposable email services give you a fixed window and that's it. I wanted users to be able to extend if they were waiting on a slow verification email.

The implementation is one API call: POST /api/extend. It updates expires_at to now + 15 minutes (from the current time, not from the original expiry). The client gets the new timestamp and resets the countdown.

I debated whether to allow unlimited extensions or cap it. Right now it's unlimited. The cleanup job still runs, so abandoned sessions die on schedule. Only sessions where the user actively clicks extend get more time. In practice, most people extend once or twice. Nobody sits there clicking extend for hours.

The frontend timer

The countdown display was trickier than expected. I wanted it to feel responsive without hammering the server.

The client calculates the remaining time from the server timestamp and uses requestAnimationFrame tied to a 1-second interval to update the display. No polling. The only time the client talks to the server about time is on initial load and when the user clicks extend.

There's a visual urgency thing too. When the timer drops below 2 minutes, the display turns red and the countdown font gets slightly larger. It's a small touch but it actually increased the extend button usage by roughly 30% compared to when the timer just quietly expired.

Edge cases that bit me

Timezone handling. The server stores UTC timestamps. The client converts to local time for display. Sounds obvious but I shipped a version where the countdown was off by several hours for users in Asia because I was comparing a UTC timestamp to a local Date object. Spent an embarrassing amount of time on that one.

Tab visibility. If the user switches tabs for 10 minutes, requestAnimationFrame stops firing. When they come back, the timer jumps from "12:00 remaining" to "2:00 remaining" instantly. I added a visibilitychange listener that recalculates on tab focus so the jump is immediate instead of waiting for the next animation frame.

Server clock drift. The VPS clock drifted by about 3 seconds over a week. Not a big deal for most applications but when your product is literally a countdown timer, users notice. I set up NTP sync and the drift went away.

Closing thoughts

A 15-minute timer is conceptually simple but it touches a lot of surface area: server state, client rendering, SMTP rejection, database cleanup, timezone math, tab lifecycle. Each piece is straightforward on its own. The bugs come from the interactions between them.

The site is 15minutemail.com. 20 languages, no ads, no tracking. If you're building something with time-based state, hopefully this saves you from the timezone bug at least.

Top comments (0)