Building DropReady: Drop Scheduling and a Bug That Was Always There (Week 6)

Published on 29 September 2026

Building DropReady: Drop Scheduling and a Bug That Was Always There (Week 6)

Building DropReady: Drop Scheduling and a Bug That Was Always There (Week 6)

Last week I added next-drop signup from inside the existing queue widget. This time I picked up something much bigger and riskier - the first change to touch the admit worker itself, the most sensitive, production-critical code in the whole system.

The problem it solves

Two things store owners keep asking for: schedule an automatic start/end for admitting visitors at an exact date and time, instead of manually flipping the pause switch to the second at noon on a Friday; and have history/stats split per drop, so you can compare "Friday's drop" against "today's." Today the data kept overwriting itself in a single, endless 60-point buffer - at a 5-second sampling rate, that's only 5 minutes of history.

The key design decision: a drop as an implicit unit

Instead of building a separate "Drop" entity with its own Start/Stop button in the UI, I defined a drop as the period between one Paused: false→true transition and the next. That's the exact same Paused field that was already being toggled manually in the dashboard. The worker (cmd/queue/main.go: startAdmitWorker) detects the transition on every tick and calls Store.StartDrop/EndDrop - zero new UI action beyond what scheduling itself already needed.

The schedule is two config fields: ScheduledStartAt/ScheduledEndAt - one-time, not recurring. The worker clears the field right after it fires, so it won't trigger again after a process restart. A deliberate asymmetry: ScheduledStartAt only takes effect while the queue is already paused - setting a future start date on a running queue doesn't surprise-pause it.

A dormant bug from before this feature, surfaced by this feature

While implementing this, I noticed something in the worker's existing code:

if !cfg.Paused && !now.Before(nextAdmitAt) {
    // ... admit visitors ...
    nextAdmitAt = now.Add(interval)
}

nextAdmitAt only got updated inside if !cfg.Paused. The worker loop picks the nearest of three deadlines (nextAdmitAt/nextSnapshotAt/nextCleanupAt) and sleeps exactly that long - a deliberate alternative to time.Ticker, which drops ticks under a slow consumer. The problem: once Paused=true held for longer than a single tick, nextAdmitAt froze in the past forever - it never got pushed forward again, because the condition guarding it never ran. The effect: the loop woke up immediately, checked the config, and went back to sleep for ~0 seconds, over and over, hammering Redis with GetConfig calls for no real purpose.

This bug had been there since the worker's very first version, just harmless as long as pauses stayed short - a quick manual pause by an admin. Scheduling introduces long pause windows between planned drops (hours, days) - exactly the scenario that would turn this bug into a real production performance problem. The fix: nextAdmitAt (and, the same way, nextSnapshotAt) must always advance by its interval when it's due, regardless of whether the actual work - admitting visitors, writing a snapshot - happened or not.

Good lesson: a "harmless bug in production" is often just a bug that hasn't met the right edge case yet, not a non-bug.

Zero-value time.Time in JSON

The second bug I only caught through live testing with curl, not unit tests with mocks. Drop.EndedAt is a time.Time, zero-valued for an ongoing drop. HandleGetDrops and HandleGetDropHistory were serializing queue.Drop straight to JSON - and a zero-value time.Time marshals by default as "0001-01-01T00:00:00Z", not as an empty string or null. A frontend expecting a simple "empty string means the drop is still running" would instead get a strange first-century date.

The fix: a new dropWire type, separate from queue.Drop, with fields passed through formatTimeOrEmpty - the exact same pattern HandleStats already used for the schedule fields in the config. The same bug nearly repeated itself in a third spot: PUT /admin/config was also echoing back the raw queue.Config with a zero-value date - caught and fixed only during manual testing against the live backend (see the verification section below), because no unit test checked it, and the frontend happened to never read that particular field from the PUT response.

Other decisions worth noting

maxHistoryPoints raised from 60 to 4320 (6h at 5s sampling). The old limit made sense when the buffer was endless and showed "the last few minutes" forever; now history belongs to a single, finite drop and should cover it in full, not just the tail end.

Schedule precision is bounded by the history snapshot cadence (5s), not by the admit interval - the schedule check runs on every loop tick, and the most frequent of the three deadlines is the 5-second snapshot. In practice: a scheduled start/end fires with a delay on the order of single seconds, not minutes.

Final verification

A start scheduled 8 seconds out on a paused queue - the worker fired the auto-start exactly on schedule, cleared the schedule field (confirming it's one-time), and started admitting visitors. Same for a scheduled end. GET /admin/drops showed two drops: one finished with a correct ended_at, one ongoing with ended_at: "" - exactly the fix from the section above, verified live.

FAQ

Does DropReady support scheduling automatic drop start and end times?
Yes. The configuration accepts a one-time start and end timestamp. The worker detects the moment on its own and switches the queue, no manual pausing at an exact time required.

Does the schedule repeat automatically?
No. It's a one-time schedule - the worker clears the field right after it fires, so it won't trigger again after a process restart.

Is drop history tracked separately for each drop?
Yes. A drop is defined as the period between one pause toggle and the next, and history/stats are stored separately for each such period instead of overwriting a single endless buffer.

DropReady

Want to see how DropReady protects your store during drops and flash sales? Check out the product page.

Michał Sobczak
Michał Sobczak https://msobczak.pl
Home About Us SaaS Blog Contact Get in Touch