Countdown Timer API vs building it yourself

A deadline table and a worker that polls it is genuinely easy, and for plenty of products it is the correct decision. This page is not an argument that you cannot build it. It is an honest list of what the finished version contains, because the gap between version one and the version you actually need is where the cost hides — and almost none of it is discovered on day one.

Version one is easy

It really is. A table, an index, and a worker. If your deadlines are internal, low volume, and nobody sees a countdown, you may never need more than this — and adopting a service for it would be overkill.

The afternoon version

// Version one. Ships in an afternoon and is genuinely fine.
CREATE TABLE deadlines (
  id          uuid PRIMARY KEY,
  record_id   uuid NOT NULL,
  expires_at  timestamptz NOT NULL,
  fired_at    timestamptz
);

-- Plus a worker that polls it.
SELECT id, record_id FROM deadlines
WHERE expires_at <= NOW() AND fired_at IS NULL
LIMIT 100;

This works until it does not, and the ways it stops working arrive in a predictable order.

Versions two through four are the actual work

What production adds

-- Version two, after the first production incident:
-- two workers processed the same rows.
SELECT id, record_id FROM deadlines
WHERE expires_at <= NOW() AND fired_at IS NULL
ORDER BY expires_at
LIMIT 100
FOR UPDATE SKIP LOCKED;   -- now correct under concurrency

-- Version three, after a downstream outage lost a day of events:
ALTER TABLE deadlines
  ADD COLUMN attempt_count int NOT NULL DEFAULT 0,
  ADD COLUMN next_retry_at timestamptz,
  ADD COLUMN last_error    text,
  ADD COLUMN response_code int;

-- Version four, after someone asked "did it actually fire?":
CREATE TABLE deadline_deliveries ( /* attempt history */ );

Every one of those changes follows an incident rather than a design review. Two workers double-processing a batch. A downstream outage that silently discarded a day of events. An operator asking whether a specific deadline fired, and nobody being able to answer.

The pattern is worth naming: none of these problems are visible at the design stage, and each one is small enough that fixing it never feels like it warrants reconsidering the approach. The cost accumulates in increments that are individually easy to justify.

The full list

What a complete implementation contains, roughly in the order teams discover each item.

CapabilityWhy it appearsRough effort
Deadline storage and indexDay oneHours
Polling workerDay oneHours
Concurrency-safe claimingTwo workers double-processed a batchA day, plus a load test
Retry with backoffA downstream outage lost eventsDays
Delivery history and attempt logSomeone asked whether it firedDays
Dead-worker alertingIt stopped silently for a weekendA day, plus a heartbeat
Idempotent creationA retried request created two deadlinesA day
Server-authoritative clock endpointThe UI disagreed with the backendDays
Countdown UI with drift handlingSleeping tabs and wrong system clocksDays
Hosted pageMarketing wanted a link to sendA week
Embeddable widgetIt needed to work on a page you do not controlA week
Email-safe animated GIFEmail clients strip JavaScriptWeeks
Per-recipient deadlinesEvergreen offers and trialsA week
Recurring schedules with DSTA daily timer drifted an hour in MarchA week, done properly
Signed callbacksThe endpoint had to be publicDays
Ongoing maintenanceForeverForever

Not every product needs every row. But the rows are not independent — a hosted page and an email image that disagree are worse than neither, so the clock authority has to be shared, which means it has to exist first.

The row that surprises people

Email is where build-it-yourself usually stops. Everything above it is ordinary backend work your team can do well. This one is a different discipline.

What the email requirement contains

// The email requirement, in full.
//
// Email clients strip JavaScript, so a live countdown in an inbox
// has to be an image generated at open time. That means:
//
//   1. An HTTP endpoint that renders the current remaining time
//   2. Text layout and font rendering, server-side
//   3. Multi-frame GIF encoding, per request
//   4. Cache headers that defeat proxy caching without
//      hammering your own renderer
//   5. Enough throughput for a send to 50,000 recipients who
//      all open within the same hour
//
// Plus the same clock authority the rest of the system uses,
// or the email disagrees with the page.

The throughput requirement is the part that catches teams late. A send to fifty thousand recipients produces a burst of image requests concentrated in the hour after delivery, each needing a freshly rendered GIF because a cached one shows the wrong time. That is a rendering service with its own scaling characteristics, built to support a feature that is not your product.

When building it yourself is right

Often. The list above is long, but plenty of products genuinely need only the top of it.

When building it yourself is the better choice

  • Your deadlines are internal and nobody sees a countdown. Most of the list never applies.
  • Scheduling is your product, or close enough to it that owning the implementation is a real advantage.
  • Data residency, air-gapping, or compliance rules out an external service handling this data.
  • You already run durable job infrastructure — Temporal, Sidekiq with reliable scheduling, a well-tested outbox — and a deadline is just another job.
  • Volume is high enough that per-timer pricing would exceed the cost of maintaining your own.
  • You need behaviour the API does not offer: custom visual design on rendered output, sub-second precision, or unusual recurrence rules.
That last point is worth being explicit about. This API deliberately does not expose colour, font, or layout controls on rendered output — timers use the product's standard presentation. If pixel-level brand control over the countdown image is a requirement, building it is the honest answer.

The middle path

It is not all-or-nothing, and the split most teams land on is by visibility.

Keep what you have for internal deadlines

If your job runner already handles retries and dead letters, internal follow-ups and cleanup tasks are fine where they are. Moving them buys nothing.

Use the API where rendering is the requirement

Customer-facing countdowns — the ones that need a page, an embed, or an email GIF that all agree with each other — are where the cost of building is concentrated and the benefit of not building is largest. One POST /timers replaces the entire bottom half of that table.

Common questions

How long does it take to build a basic version?

A table and a polling worker is an afternoon, and for a low-stakes internal deadline that is often the right answer. What takes longer is everything that follows the first incident: concurrency-safe claiming, retry state, delivery history, alerting on a dead worker, and a clock the front end can trust. Those are weeks, spread over months, discovered one at a time.

What is the hardest part to build?

The email GIF, by a wide margin. Server-side text layout, frame encoding, and doing it fast enough for a large send is a specialist problem that has nothing to do with your product. Second hardest is delivery semantics — at-least-once with retries, deduplication, and a replay path is subtle to get right and easy to get subtly wrong.

Is it cheaper to build?

Not usually, once you count engineering time and ongoing maintenance rather than hosting. But cost is rarely the deciding factor. The real question is whether deadline infrastructure is something your team should be differentiating on, or whether that attention belongs on your actual product.

What if we already built it?

Then keep it, if it works. The case for switching is not "yours is bad" — it is whether you are still paying maintenance on something that has stopped being interesting. A reasonable middle path is to keep your existing system for internal deadlines and use an API only where you need rendering you have not built.

Related

Try it before you scope the build

Sandbox is free with any account. Half an hour against the real API is a cheaper way to size the decision than a design document.