NuevoVer cómo
Todas las novedades
Corrección5 min de lectura

Scheduled sends stop colliding

Two emails could claim the same sending slot and one would be dropped without a trace. Slots are now assigned so collisions cannot happen, and a rejected claim is reported instead of swallowed.

Two emails could be scheduled into the same sending slot, and when that happened one of them disappeared without leaving a record. Slots are now allocated so two messages cannot claim the same one, and the rare rejection that does occur is reported rather than silently absorbed.

What changed

Sending is spread across the hours you allow, so that a campaign does not fire a hundred messages in one burst. Each message gets a slot, which is a specific minute in a specific hour, and the slot is what keeps sending looking like a person rather than a machine.

Slots were being calculated in a way that could produce the same one twice for two different messages. The database correctly refused the second one, because two things cannot occupy the same slot, and the refusal was then caught and thrown away by code that assumed it could not happen.

So the second message was never sent and nothing recorded that it had not been. The campaign showed it as scheduled forever, and the only visible symptom was a send count slightly lower than it should have been, which is not a symptom anyone notices.

The slot calculation now works in a single consistent time reference, which removes the source of the duplicates. And the refusal path no longer swallows anything: a claim that cannot be honoured is logged, and the message is given a different slot rather than dropped.

Why it matters

A dropped email is not recoverable after the fact, because nothing knows it was dropped. There is no queue of failures to work through and no error to investigate. The message simply never happens.

The scale was small, which is part of why it survived. A handful of messages across a busy workspace is invisible in the totals and indistinguishable from ordinary variance in campaign size.

That invisibility is the reason it was worth fixing properly rather than adding a retry. A bug that costs a little and reports nothing is a bug that stays for years, and the total across those years is not small.

Why it happened at all

The two ends of the calculation disagreed about time. One worked in the workspace's local hours and the other in a fixed reference, and for most of the day the two produce the same answer.

They diverge at the boundaries: the hour a clock change lands on, the hour a day rolls over, the hour a schedule crosses midnight in one reference but not the other. Those are the hours where two messages could be handed the same slot.

Doing the whole calculation in one reference removes the disagreement. The conversion to local hours happens once, at the point where a person needs to read the time, rather than in the middle of the arithmetic.

What you will notice

Send counts that match what the campaign was asked to do. If a campaign was set to send four hundred messages, four hundred messages go out.

Nothing about scheduling changes from your side. The same sending windows, the same spacing, the same behaviour. What changed is underneath.

Messages already scheduled were unaffected by the fix and continue as planned. Nothing needed rescheduling.

Clock changes

Twice a year the clocks move, and a sending window either loses an hour or gains one. This is where scheduling code historically goes wrong, and it was where this particular problem was most likely to appear.

Sending now handles the shift without dropping or duplicating anything. A window that loses an hour sends slightly less that day. One that gains an hour sends slightly more. Neither produces a collision.

If you send across several time zones, each workspace's windows are interpreted in its own zone rather than a shared one, so a clock change in one country does not shift sending for another.

What was checked afterwards

The fix went out with a test that reproduces the collision, so a future change that reintroduces it fails before it ships rather than after.

Sending volumes were compared against expected volumes across a range of campaigns after deployment, and the small persistent shortfall that had been visible in that comparison is gone.

How a bug like this hides

Every part of the system behaved correctly in isolation. The database refused a duplicate, which is exactly what a database should do. The code caught the refusal, which is what code that talks to a database is supposed to do. The problem was what happened after the catch: nothing.

A caught error that is never reported is worse than an uncaught one. An uncaught error crashes something and somebody finds out. A caught and discarded error produces a system that appears to be working perfectly while quietly losing a fraction of its work.

The lesson generalises beyond this fix. Anywhere a failure is caught, something has to happen next, even if that something is only a line in a log saying the failure occurred. Silence is a decision, and it is almost always the wrong one.

What to check on your side

Nothing, really. There is no cleanup to do and no messages to re-queue, because messages affected by this were never scheduled in a recoverable state.

If you keep your own record of how many emails a campaign should have sent, comparing it against the reported sends going forward should now match. Before, it would have been short by a small and unpredictable amount.

Historic figures are not retroactively corrected. Sends that never happened cannot be added to a past period, so any comparison across the fix date will show a small step.

Availability

Live now on all plans. Nothing to configure and no action needed on existing campaigns.