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:

  1. 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 existing campaign_send_jobs pipeline. 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.
  2. 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:

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:

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:

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:

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:

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:

  1. Send invite
  2. Wait 7 days for accept
  3. If accepted, send first message
  4. Wait 3 days
  5. 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):

V15 timeline (the new design):

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:

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.

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.