Operations · · 4 min read
Why is my Slack bot sending duplicate messages?
Separate Slack event retries, duplicate workers, and repeated sends. Diagnose message duplication without throwing away legitimate events.
Two identical answers appear in a Slack thread. Changing the prompt so the agent promises to reply once does not help: a duplicate can happen before the model runs, after it finishes, or while a worker recovers from a timeout, and each needs a different fix.
A Bolt Python issue about duplicate messages shows why inbound delivery comes first: a maintainer pointed to repeated message events and slow acknowledgments as a likely cause. Begin with the event and outbound request identifiers.
Why does one Slack message produce multiple bot replies?#
Follow the sequence (inbound event, accepted job, worker attempt, model result, outbound send, saved message reference) and count each transition for the same triggering event.
- Multiple deliveries but one accepted job: the inbound layer is doing its job.
- One job producing two outbound requests: inspect worker retries or the send step.
- Two independent jobs: inspect routing and deduplication.
Keep in the diagnostic log the installation, channel, event ID, job ID, attempt number, and returned message timestamp.
Does Slack retry events when my agent takes too long?#
Yes. Slack's Events API documentation describes HTTP acknowledgments, retry delivery, and retry headers. Long model work should not sit on the acknowledgment path. The event envelope includes an event_id that identifies a repeated delivery.
Verify the request, durably accept the event, and acknowledge it promptly. A worker then does the expensive work. "Durably" matters: acknowledging before an in-memory task survives a crash turns a duplicate-message fix into missing messages.
Treat queue acceptance and the HTTP response as separate observations: if durable acceptance succeeds but the acknowledgment is lost, the next delivery finds the existing job.
Should I discard every event with a retry header?#
No. A retry can be the first delivery your application successfully processes, so decide from the event's durable state.
Give each event states such as accepted, running, completed, and needs reconciliation. Use an atomic insert or equivalent unique operation to assign ownership. Two workers doing "look up ID, then insert ID" can both pass the lookup before either inserts.
| Existing state | Useful next action |
|---|---|
| No accepted event | Accept it once and schedule processing |
| Accepted, no live worker | Recover the existing job |
| Running with a valid lease | Avoid a second worker |
| Completed with message reference | Return the saved outcome |
| Outbound send outcome unknown | Reconcile before sending again |
Why can duplicates continue after I deduplicate events?#
Inbound deduplication does not make an outbound write atomic with your database. A worker sends the answer successfully and crashes before saving the returned message reference. On restart it sees an unfinished job and sends again.
Persist a distinct outbound action before dispatch, with its destination and intended content version. Save the provider response against that action. If the response is lost, represent the state as uncertain and use the message history and correlation data to investigate. Do not mark it failed because your worker missed the response.
Slack's message posting reference describes the destination and thread parameters and returned message information. Keep the channel and timestamp when a send succeeds. A locally generated action ID helps coordinate attempts and gives no provider guarantee against duplicate delivery.
How do I reply in the original thread rather than starting another one?#
Carry the destination with the job. For a reply, use the parent message's thread timestamp and the intended channel.
In your tests, deliver the same inbound event twice, run competing workers, crash after queue acceptance, and simulate a lost outbound response. Also test two legitimate user messages with identical text, since content similarity must not erase the second request.
Separate transport duplicates from an agent independently asking the same question tomorrow, which needs a retained answer and source reference, as explained in keeping Slack questions answered. The Slack connector lists its reads and writes.
Find another symptom in the agent troubleshooting question index.