Zum Inhalt springen
NeuSo funktioniert es
Fehlerbehebung

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.

Alle Neuigkeiten

Akquise, die weiterläuft, wenn du offline bist.Leads, E-Mails, Follow-ups und Antworten, erledigt.

Verbinde unbegrenzte Postfächer und lass KI-Agenten Leads finden, jede E-Mail schreiben und nachfassen. Oder sende deine eigenen Vorlagen im manuellen Modus. Ab $29/Monat.

$1 für 3 Tage · Jederzeit kündbar · Kein Vertrag

Mit KI installieren

Dein Coding-Agent richtet die FirstSales CLI und Skills für dich ein.

Prompt für Claude Code

I'm setting up FirstSales (https://firstsales.io), an AI outbound sales platform: campaigns, contacts, a reply inbox, deliverability and lead signals. Help me get my workspace sending real, good emails end to end, from Claude Code.

Read these first so you know how FirstSales works:
- Developer docs and API: https://developer.firstsales.io
- CLI: https://www.npmjs.com/package/@firstsales.io/cli
- Skills for coding agents: https://github.com/firstsalesio/firstsales-skills (read README.md and GUIDE.md)

Setup
1. Install the CLI: npm install -g @firstsales.io/cli
2. Install the FirstSales skills:
   claude plugin marketplace add firstsalesio/firstsales-skills
   claude plugin install firstsales@firstsales
3. Ask me if I already have a FirstSales account. If not, send me to https://app.firstsales.io to sign up.
4. Ask me to create an API key under Settings → API → API Keys, with only the scopes we need.
   Never ask me to paste the key into this chat. Tell me to run this in my own terminal, so the key stays hidden:
   read -rs FS_KEY && firstsales auth login --api-key "$FS_KEY"; unset FS_KEY
5. Run `firstsales whoami` and tell me which workspace and organisation we are connected to.

Then ask me about my business: what I sell, who I sell to (roles, company size, countries), my best customers, and what I use today for outbound (tools, lists, mailboxes). Walk me through:
1. Teaching FirstSales my business: website, offer, proof points and case studies in the knowledge base, so the AI writes like someone who knows the product.
2. Connecting my sending mailboxes and checking my sending domain: SPF, DKIM, DMARC and warm-up. If anything is unhealthy, tell me the exact DNS record to add.
3. Bringing in contacts: import my CSV or CRM export into a clean list, remove duplicates and bad emails, and block domains I must never email (customers, competitors, my own).
4. Building my first campaign: the right list, senders, steps and timing. Write the sequence with me, show me the drafts, and do not launch until I say yes.
5. Setting up Signals if it fits: find people engaging with LinkedIn posts about topics my buyers care about, and turn them into leads.
6. Inviting my team and giving each person the right access.

Once campaigns run, show me how to work the reply inbox from here: which replies need an answer, draft responses in my voice, and move good conversations into deals. If I use Cal.com, connect it so follow-ups can offer my booking link (not the first email).

Throughout, as you learn about my business, proactively suggest and help create:
- Reply templates for the questions and objections my prospects are likely to raise, based on my product, site and codebase if you can see them.
- Campaign ideas and segments worth testing, with the reason for each.

Safety rules:
- Show every change as a dry run (--dry-run) first and wait for my yes. Never send emails, launch campaigns or delete anything without my yes.
- Tell me before anything that spends credits, and how many.

Be opinionated. If the docs give a sensible default, just use it. Don't make me choose between five options.