EmailTimer.App

Building a countdown timer yourself, counted from the code

By Danish Mohammed, founder. 7 min read

Doing it yourself means one of three things. You can write the date into a static image, or export an animated GIF once and drop it into the template. Or you can run a small server that draws the countdown each time a mail client asks for the picture.

The first two cost almost nothing and fail in ways you can predict. The third is the one that competes with buying a product like this one. I can count what it takes, because it is what I built, and every figure below comes from this product’s repository as it stood on 17 September 2026.

A static image with the date on it

A picture reading “Sale ends Friday 23:59” is accurate, small and safe in every client. It is not a countdown. It says the same thing on Thursday morning as it does an hour before the close.

For a lot of sends that is enough. If the offer runs for a week and the email goes out once, write the date in text and skip the image. Text survives blocked images and screen readers, and it cannot go stale.

A GIF made once

A downloaded GIF counts from the moment its frames were drawn. The free generator here says so under its own download button: “A downloaded GIF counts from the moment it was made, not from the moment it is opened.”

Sent an hour after it was made, the file is an hour wrong for everyone. Opened the next morning, it is a night wrong. Nothing inside the file can correct that. The GIF89a specification gives a frame a delay, measured in “hundredths (1/100) of a second”, and a GIF has no field for the time of day. A countdown exported from a design tool has the same limit, which the Canva countdown page goes through.

A renderer of your own

The version to compare against a product is a web service. The email carries an image address, a mail client fetches it, and the service works out the time left and draws a GIF on the spot. The idea fits in one paragraph. The table lists what it took to make it safe to send to a real list.

A renderer of your own
Job What it involves here Where it lives
Draw the countdown when the image is fetched A composed background, a sheet of digit sprites and a GIF assembler. 30 frames by default, 20 to 60 allowed, one frame a second packages/engine
Make frame one a finished picture Frame one is the whole canvas with the live numbers painted in. Microsoft’s support page says that when only the first frame appears, “you have animations, or animated GIFs, disabled” packages/engine/src/gif.ts
Keep the file small Frames after the first repaint only the digit cells that changed. The engine’s notes record a local benchmark of that change: assembly from 24.9 ms to 5.2 ms median, and the file from 56 KB to 10.8 KB, at 600 by 200 and 30 frames packages/engine/src/gif.ts
Stop anyone editing the address HMAC-SHA256 over the path and a canonical query. It is written twice, for Node and for Web Crypto in an edge Worker, and 17 shared test vectors pin the two together packages/engine/src/sign.ts, apps/edge/src/sign.ts
Get time zones right Location names such as Europe/London, plus UTC and GMT. Etc/GMT+5, SystemV zones and abbreviations such as EST are refused, because each one freezes daylight saving or inverts a sign packages/engine/src/time.ts
Start a clock on first open A first-seen record per address in Redis, written with one atomic SET using NX and GET, kept for the timer’s length plus 30 days and never longer than a year apps/img/src/evergreen.ts
Never show a broken image Every request is answered HTTP 200 with a picture. There are three fallback layers, the last a hard-coded 1 by 1 PNG. A render still running after 5,000 ms is answered with the fallback, and so is new work past 64 renders in flight apps/img/src/fallback.ts, apps/img/src/limits.ts
Tell caches how long to keep a render For a live timer the service writes public, max-age=0, s-maxage=30, no-transform, must-revalidate and a strong ETag, so a repeat fetch of identical bytes can get a 304 apps/img/src/cache-headers.ts
Stop cleanly at the deadline An expired timer is a single frame of zeros or an image you choose. Its headers allow a day in a private cache and a week in a shared one packages/engine/src/render.ts, apps/img/src/cache-headers.ts
Refuse a template that cannot render Publishing renders the template first, and refuses it for an unusable time zone, a deadline already past, a missing asset, an unknown font or a render that fell back apps/api/src/publish-gate.ts
Keep sent emails working after an edit Published versions are append-only, and every image address carries the version it was sent with inside the signature packages/shared/src/url-params.ts
Frame one of a live timer: Everything is 30% off for 24 hours, 01 days 11 hours 59 minutes 00 seconds An expired timer drawn as one frame with every unit at 00
Two rows of the table as the engine here draws them: frame one of a live timer with the numbers painted in, and the single all zeros frame served after a deadline.

What the table leaves out

Daylight saving is the first. A weekly deadline at 09:00 that crosses a clock change is 167 or 169 real hours away, not 168, and the engine has a test for each direction.

The second is silence. A signature that fails, a template that cannot be read or a store that times out all end in a grey “image unavailable” card at HTTP 200. That is the right answer for a delivered email. It also means nothing in a request log looks like an error, so you have to build the alerting on purpose. A merge tag pasted inside a signed address shows why: the platform fills it in after signing, verification fails for every recipient, and every one of them gets that card.

The third is load. An audit on 16 September 2026 estimated the image service, as it stood then, at about 80 opens a second per process, and one send to a large list outruns that. The changes that followed were a GIF cache shared by everyone who opens in the same second, and one render per key however many requests wait on it. They also added several worker processes, and timeouts that shed to the grey “image unavailable” card.

How much code that is

Counted at commit 170b79f with wc -l over the TypeScript files, comments included, and this repository comments heavily. Only the render path is counted: the engine, the image service, the shared schema and an edge Worker that is built but not yet routed in front of the image service. The editor, the API, billing, CSV import and Photoshop import are left out.

How much code that is
Package Source lines Test lines Test files
packages/engine 8,618 10,365 17
apps/img 4,898 6,164 20
apps/edge 1,463 1,883 7
packages/shared 1,395 1,114 5
Total 16,374 19,526 49

Across those 49 files there are about 1,300 it( and test( calls, give or take a few dozen depending on how parameterised tests are counted. The test lines outnumber the source lines. A failure here happens inside an email that was already delivered and cannot be recalled, which is why so much of the code is tests.

What buying costs instead

These are the published plans. Prices exclude tax. An open is one request for the image, and annual billing is ten months’ price for twelve. What each plan includes is on pricing.

What buying costs instead
Plan Per month Opens a month
Free $0 5,000
Starter $19 250,000
Growth $49 1,000,000
Agency $149 5,000,000

Past 110 percent of the allowance, new renders carry a small watermark until the plan changes. Nothing is switched off mid-send.

I am not putting a salary on this page. Day rates vary by country and by person, so the only figure that means anything is yours. Put your own rate against the tables above, then add the cost that is easy to leave out: someone to notice when a render goes wrong on a Saturday.

When building it yourself is the right call

If you need more than 5,000,000 opens a month, there is no larger plan here and no enterprise contract either. A service of your own is the sensible route at that size.

If your security review needs the image service inside your own network, this is the wrong product. There is no self-hosted edition, and there is no SOC 2 report or ISO 27001 certificate.

If you want a hosted tool but not this one, the others are compared, with their prices and the date they were read, in the best email countdown timers.

If you only need people to know the date, you need neither. Write it in the email.

Questions and answers

Can I make a countdown GIF with a script and host it myself?

Yes, provided the script runs when the image is fetched rather than once before the send. The drawing is the easy half. Signing the address, answering every failure with a picture and setting cache headers are where the time goes.

Why does a timer I built show the wrong number on a second open?

Usually because a proxy or a mail app served a copy it kept. Your response headers decide how long a shared cache may reuse a render, and each client’s own behaviour, with its sources, is in how email image caching works.

Is there a self-hosted version of EmailTimer.App?

No. The price list has four hosted plans and nothing to install.

Can I keep templates in my own code and still use this?

On Growth and Agency. The REST API creates and publishes templates and mints up to 10,000 signed image addresses in one call, and the API reference lists every endpoint. If you want to try the renderer before any of that, the free generator makes a live timer without an account.

Sources

Related

Put a countdown in the campaign you are writing now

The free plan covers 5,000 opens a month with no card. Starter is $19 a month for 250,000.