Why a late LinkedIn accept may take time to message
What this page covers
This page explains the timing semantics of the WarmySender follow-up message dispatch path when a LinkedIn invitation is accepted after the campaign workflow has timed out, condition-routed, or completed (a "late accept"). It covers what "late accept" means, why follow-up messages used to take up to 6 hours after an accept arrived, what changed in the V15 (May 7, 2026) deploy so dispatch now happens at webhook time instead of via the 6-hour heal scheduler, and how the heal scheduler still functions as a backstop.
WarmySender is a 4-pillar outreach platform — Cold Emailing, Email Warmup, LinkedIn Outreach, and Multichannel sequences. This page is part of the LinkedIn Outreach pillar and covers operational behavior of the auto-fire follow-up dispatcher.
Recap — what is a "late accept"?
A late accept is when a prospect accepts your LinkedIn connection invite after your campaign's workflow has already advanced past the wait_accept step (timed out), routed to a condition's false-branch (the prospect's linkedin_status is condition_skipped), or the campaign itself has completed. See What is a "late accept" for the full definition and the dashboard tile that surfaces them.
The acceptance webhook fires identically whether the accept is in-time or late — the same payload, the same delivery latency. What differs is what our system does with the accept once it arrives.
Two dispatch paths — webhook-time vs heal-time
For an in-time accept (workflow still active), the engine advances the prospect to the next step (typically a follow-up message) on the very next scheduler tick — usually within seconds, certainly within minutes. The follow-up message lands in your prospect's inbox according to your campaign's sending-window and per-account ramp.
For a late accept, the routing is different because the workflow has already moved on. There are two possible paths:
- Webhook-time auto-fire (preferred — V15 default). When the late-accept webhook arrives, our handler stamps
late_accept_observed_at, then immediately calls the auto-fire dispatcher to enqueue ONE follow-up message via the existingcampaign_send_jobspipeline. The pipeline respects all per-account safety gates (caps, ramps, cooldowns, sending windows) at job execution time. From your perspective: the follow-up message arrives within minutes of the accept, just like an in-time accept. - Heal-time backstop (every 6 hours). A recurring background sweeper runs every 6 hours and looks for late-accept rows that didn't get a follow-up via path 1 (rare — would happen if the dispatcher's pre-flight check rejected the dispatch, e.g. if the LinkedIn account was disconnected at webhook time but reconnected later). The backstop catches anything path 1 missed and dispatches the follow-up. Customer-facing latency: up to 6 hours from accept to first dispatch attempt.
Pre-V15, only path 2 was active for late accepts. The webhook handler stamped late_accept_observed_at but did NOT auto-fire — it relied entirely on the 6-hour sweeper to dispatch. That meant customers waited up to 6 hours after a late accept before the follow-up message went out, even when their LinkedIn account was healthy and their daily cap was wide open.
V15 (May 7, 2026) flips the default: webhook-time auto-fire is now the primary path. The 6-hour sweeper is preserved as a backstop for edge cases.
Why isn't the message instant even with V15?
The auto-fire dispatcher creates a queued job immediately, but the engine doesn't actually send the message at the exact same instant. Three pacing layers apply:
- Job pickup tick (sub-minute). The engine polls the queue every few seconds. Your follow-up job is picked up on the next tick.
- Sending window clamp. If the accept webhook arrives at 23:00 your local time and your campaign window is 09:00–17:00, the job is queued for 09:00 the next day. This isn't a delay — it's your authored campaign configuration. Sending outside the window would breach your authored intent.
- Per-account ramp + cap. If your daily message cap is at 80/80 already today, the follow-up is deferred to tomorrow's first slot. If your account is in the cooldown ramp (e.g. days 1–14 of warmup), the per-day limit is conservatively low and the follow-up may queue behind other prospects.
So the V15 fix doesn't promise "instant" — it promises "as fast as your campaign configuration and per-account safety allow." For most users in steady-state with a healthy account and an open cap, that means within minutes of the accept landing in your account during a sending-window.
How the 6-hour backstop works (heal scheduler)
The heal scheduler is a recurring background process that runs two ticks every 6 hours, staggered 30 minutes apart so they never run at exactly the same wall-clock minute:
- Late-accepts tick. Scans for prospects with
late_accept_observed_at IS NOT NULLANDlate_accept_followup_sent_at IS NULLAND a recentbridge_miss_all_strategiestrace. For each, it stamps the late accept (idempotent — a no-op if already stamped) and dispatches the follow-up via the same pipeline path the webhook-time auto-fire uses. Hard-cap 200 rows per tick. - Stuck post-accepts tick. Scans for prospects whose
linkedin_accepted_atis between 24 hours and 14 days old AND no follow-up message has been sent yet AND the campaign is running. Catches the rare case where an in-time accept didn't trigger a follow-up due to a transient pipeline failure (e.g. orphan-webhook race, queue retention drop, etc.). Hard-cap 200 rows per tick.
Both ticks are idempotent: running them twice in a row is a no-op on the second run because the prospect column will be stamped after the first dispatch (per the V14 stamp-on-completion design — the column is set by the worker on send-success, not at enqueue time, so the heal can re-run if the first job expired without delivery). They are also distributed-lock-protected: a multi-pod deploy doesn't double-heal because a distributed lock with TTL ensures only one pod runs each tick.
V15 added a third tick — the hourly skip-retry tick — that re-attempts post-acceptance enqueue helpers that hit a recoverable skip in the past 24 hours (campaign paused, condition step skipped, concurrent-advance race). Hard-cap 100 rows per tick. This catches edge cases where the immediate enqueue was blocked by a transient state (e.g. user paused the campaign briefly, then resumed).
Why the old design used a 6-hour sweep
Pre-V15, the conservative design was: stamp the late accept on the database, but rely on the heal scheduler to dispatch follow-ups in batch every 6 hours. Reasoning at the time:
- Account-safety conservatism. Late accepts disproportionately come from prospects whose campaign workflow has routed to
condition_skipped, terminal states, or completed campaigns. Auto-firing a follow-up at webhook time on those rows could (in theory) re-arm forward-movers that readlinkedin_accepted_at, which (per LL#265/LL#266/LL#271) re-introduces thecannot_message_non_connectionretry storm those rules were specifically built to prevent. The 6h batch dispatch was a "let's stamp first, dispatch in batch when ops can monitor" stance. - Idempotency double-protection wasn't fully designed yet. The dispatcher's NOT-EXISTS guard on active
campaign_send_jobsrows AND the partial unique index on(enrollment_id, step_index) WHERE status IN ('pending', 'processing')were not yet hardened. The 6h batch dispatch ran once per cycle, so there was less concern about double-fire than with a webhook-time path.
By V15, both concerns were resolved. The dispatcher's pre-flight checks (account_status='connected', campaign_status='running', source-specific column null check, job-level NOT-EXISTS) plus the partial unique index together guarantee that auto-fire creates AT MOST one follow-up job per prospect per accept signal. The webhook-time path is now equally safe and dramatically faster.
Account safety
The webhook-time auto-fire path is read-only at the LinkedIn boundary. When the late-accept webhook arrives, we:
- Match the webhook to the prospect (database read).
- Stamp
late_accept_observed_at+ write alinkedin_eventsaudit row (database write only — no API call). - Insert a
campaign_send_jobsrow for the follow-up message (database write only — no API call). - Surface to the dashboard via existing read paths (no new query against LinkedIn).
The actual LinkedIn message-send call happens later, when the engine picks up the queued job. By that point, every per-account safety gate applies:
- Daily cap. If your message cap is exhausted for today, the job is deferred to tomorrow's first slot. No burst, no cap breach.
- Per-account ramp. If you're in the early days of warmup (e.g. days 1–14), the conservative ramp ceiling applies. The follow-up queues behind other prospects up to the day's safe ceiling.
- Sending window. If the accept lands outside your campaign's configured window (e.g. 23:00 vs 09:00–17:00), the job queues for the next valid window. Per-prospect minute jitter is applied so concurrent dispatches don't fire simultaneously.
- Account status. If your LinkedIn account flips to disconnected between the webhook and the worker pickup, the job auto-pauses and resumes when you reconnect — same flow as any other queued job.
The V15 webhook-time path does NOT introduce ANY new automatic LinkedIn API calls beyond what the existing campaign engine already makes. The dispatcher is a pure database insert into the job queue; the engine then picks it up exactly like any other job.
Worked example
You have a LinkedIn campaign with the following steps:
- Send invite
- Wait 7 days for accept
- If accepted, send first message
- Wait 3 days
- Send second message
You send an invite to a prospect on October 1. They don't accept by October 8 (day 7), so step 2 times out and the prospect routes to a fail-branch (or terminal completion). On October 10 at 14:30, the prospect accepts your invite — a "late accept" by 2 days.
Pre-V15 timeline (the old design):
- October 10, 14:30:00 — late-accept webhook arrives.
- October 10, 14:30:01 — our handler stamps
late_accept_observed_at = '2026-10-10 14:30:00'. - October 10, 14:30:01 — handler returns. No follow-up enqueued.
- October 10, ~18:00 — heal scheduler tick runs (every 6h). Picks up the late accept. Enqueues the first follow-up message.
- October 10, ~18:01 — engine picks up the queued job. If your sending window is 09:00–17:00, the job is deferred to October 11, 09:00.
- October 11, 09:00–09:05 — first follow-up message sends (per cap + ramp + jitter).
- Customer-facing wall-clock: ~18.5 hours from accept to message.
V15 timeline (the new design):
- October 10, 14:30:00 — late-accept webhook arrives.
- October 10, 14:30:01 — our handler stamps
late_accept_observed_at = '2026-10-10 14:30:00'AND immediately calls the auto-fire dispatcher. - October 10, 14:30:01 — dispatcher inserts the follow-up job into the queue.
- October 10, 14:30:05 — engine picks up the queued job (within the next polling tick). If sending window is 09:00–17:00 and current time is 14:30, the window is open — job runs immediately.
- October 10, 14:30:08 — first follow-up message sends (per cap + ramp + jitter).
- Customer-facing wall-clock: ~8 seconds from accept to message.
That's the V15 win: from 18.5 hours to 8 seconds in the median case, with no change to per-account safety. The 6-hour heal scheduler still runs every 6 hours as a backstop; it now finds 0 rows on most ticks because path 1 caught everything.
When the message may still take hours
V15 fixes the median case but doesn't promise "fast" in every scenario. Cases where a follow-up message after a late accept may still take longer than minutes:
- Accept arrives outside your sending window. If your campaign sends 09:00–17:00 and the accept lands at 23:00, the job queues for 09:00 the next morning. This is your authored intent — we don't override your sending window even on auto-fire.
- Daily cap exhausted. If your daily message cap is 80 and you've already sent 80 today, the follow-up queues for tomorrow.
- Account disconnected. If your LinkedIn account flipped to disconnected (token expired, manual disconnect, upstream session issue), the job auto-pauses. When you reconnect, all queued jobs resume.
- Campaign paused. If you paused the campaign before the accept arrived, the dispatcher's pre-flight check returns
campaign_pausedand does NOT enqueue. When you resume, our resume-sweep + the new V15 hourly skip-retry tick catches the prospect and dispatches. - Per-account ramp ceiling reached. Early in warmup (days 1–14), the conservative ramp ceiling may queue follow-ups behind earlier prospects. They'll go out as cap allows.
In each case, the customer-facing message latency is by your authored configuration, not because we're sitting on the message. The dashboard surfaces these states clearly — if a follow-up is queued for a future window, the prospect detail page shows the scheduled run time.
Common questions
My late-accept follow-up was sent within seconds — how is that different from an in-time accept?
Functionally identical from the prospect's perspective. The difference is purely how our system tracks the accept. An in-time accept stamps linkedin_accepted_at + advances the workflow normally. A late accept stamps late_accept_observed_at + auto-fires a follow-up via the dispatcher. Both result in a single follow-up message being sent through LinkedIn with the same per-account safety gates. The dashboard shows the late accept on the dedicated "Late accepts" tile so you have audit visibility, but operationally the message arrival is the same.
My follow-up still took 6 hours after a late accept — what's wrong?
If V15 is deployed and you're still seeing 6-hour latency, one of three things is happening: (1) your campaign was paused at the moment the late-accept webhook arrived, so the dispatcher returned campaign_paused; the V15 hourly skip-retry tick catches this within the hour after you resume; (2) your LinkedIn account was disconnected at webhook time; the dispatcher returned account_not_connected; the heal scheduler catches it on the next 6h tick after you reconnect; (3) your daily cap was exhausted; the job queued for the next day's first slot. The dashboard's prospect detail page shows the scheduled run time so you can see exactly when the follow-up will fire.
Does the V15 webhook-time auto-fire bypass any safety gates?
No. The dispatcher inserts a campaign_send_jobs row; the engine then picks up the job and runs it through every existing safety gate at execution time — daily cap, per-account ramp, sending-window clamp, per-prospect cooldown, account status. Nothing is bypassed. The V15 change is purely about enqueue timing (webhook-time vs 6h-tick-time), not gate enforcement.
Can I disable the auto-fire and only use the 6-hour heal scheduler?
Not via a per-campaign toggle, but the system supports it via the LINKEDIN_RECURRING_HEALS_ENABLED=false environment variable (turns off the heal scheduler entirely). For campaign-level fine-grained control, the right tool is your campaign's wait_accept window — lengthen it (e.g., from 7 to 14 days) to let more accepts arrive while the workflow is still active. That moves accepts from "late" to "in-time" and uses the normal advance path, not the late-accept auto-fire path.
What's the difference between a late accept and a bridge miss?
A late accept is when the matcher DID find the prospect, but the prospect's workflow has already moved past wait_accept (timed out, condition_skipped, completed, or stopped). A bridge miss is when the matcher couldn't tie the accept to any prospect in any of the workspace's running campaigns at all. Both are surfaced on the dashboard, both have their own trace branch, and both have their own dedicated heal path. The late-accept case is what this page covers; the bridge-miss case is about the matcher itself.
Does the V15 hourly skip-retry tick re-attempt failed LinkedIn sends?
No. The skip-retry tick re-attempts the post-acceptance ENQUEUE step, not the SEND step. If the enqueue helper hit a recoverable skip (campaign paused, condition step skipped, concurrent-advance race) the tick re-runs the helper hourly to see if the recoverable condition has cleared. If yes, a new campaign_send_jobs row is inserted. The send itself goes through the engine's normal retry policies and circuit breaker — those are unchanged in V15. The skip-retry tick is purely about the dispatcher's pre-flight rejections, not about the worker's send-time failures.
Will the V15 webhook-time auto-fire send extra messages that the user didn't want?
No. The dispatcher's pre-flight checks include source-specific idempotency on late_accept_followup_sent_at (prevents the same late accept from firing two follow-ups) AND a job-level NOT-EXISTS guard (prevents a duplicate active job for the same prospect at the same step). Plus the partial unique index on campaign_send_jobs at the database level. Together these guarantee at most one follow-up message per late-accept signal. Per V14's stamp-on-completion design, the column is set by the worker on send-success — so a job that gets enqueued, fails, and re-enqueues will correctly send the message exactly once.
If the heal scheduler is now a backstop, should I disable it?
No — keep it on. The heal scheduler catches edge cases the webhook-time path can't: late accepts on accounts that were disconnected at webhook time but reconnected later, prospects that were paused at webhook time and resumed afterward, accept events lost due to upstream webhook delivery delays, etc. The 6-hour cadence is conservative — most ticks now find 0 rows because path 1 caught everything. Leave it running; it adds no load when there's nothing to do.
Related guides
- What is a "late accept"? — Definition + dashboard tile + manual-followup CTA
- What is a "bridge miss"? — When the matcher can't tie the accept to a prospect
- Why some accept webhooks are not actioned — Orphan-webhook handler (workspace has no running campaigns)
- Accepted but no follow-up message — Three scenarios for the broader "accepted but no follow-up" pattern
- How cap enforcement works — Pre-enqueue cap peek + per-account daily limit
- How WarmySender handles load — Queue architecture + recurring heals + cache-layer best practices
- LinkedIn rate limits — Per-account daily and weekly limits
- LinkedIn campaign documentation — Schedule, sending windows, ramp, acceptance lag
- Full documentation — All 90+ guides
- Support — How to get in touch
Still have questions? Email hello@warmysender.com with the campaign name and a sample prospect — we'll dig into the timing and confirm the auto-fire path lit up correctly.