Back to Blog
Guide

Slack Events API Retries: Measured Timing and How to Stop Them

We made a Slack Events API endpoint fail on purpose and logged every redelivery with its X-Slack-Retry-Num and X-Slack-Retry-Reason headers. Slack retried 3 times, at about 0 s, 1 minute and 6 minutes. Measured timing, what X-Slack-No-Retry does, and a handler that stops the retries.

Slack Green Team
October 1, 2026
October 1, 2026
4 min read
Share:
slack api
developers
python

Slack retries an Events API delivery when your endpoint does not answer with HTTP 2xx within about 3 seconds. In our test it sent the same event 3 more times: right away, about 1 minute later, and about 5 minutes after that. Each retry carries X-Slack-Retry-Num (1, 2, 3) and X-Slack-Retry-Reason (http_timeout or http_error). Every retry has the same event_id as the first delivery, so deduplicate on it. We measured this on 1 October 2026 with a test app whose Request URL pointed at a Flask server behind a cloudflared tunnel.

What we measured

We posted one message per test into a DM with our app. The message.im event went to a handler that failed in a different way each time. The times are seconds from the post to each delivery:

Handler behaviourDeliveries (s after the post)Retry reason
Sleeps 5 s, then 2000.2 s, 3.7 s, 66.5 s, 369.7 shttp_timeout
Returns HTTP 5000.3 s, 0.6 s, 61.4 s, 362.0 shttp_error
Sleeps 4 s, then 200 with X-Slack-No-Retry: 10.2 s, 3.2 s, 66.8 s, 369.6 shttp_timeout
Returns 500 with X-Slack-No-Retry: 1 at once0.2 snone, no retry
Sleeps 2 s, then 2000.1 snone, no retry
Timeline of event deliveries over 400 seconds: the 5-second-sleep, 500 and late No-Retry handlers each get retries #1 near 0 s, #2 near 60 s and #3 near 365 s; the immediate 500 with X-Slack-No-Retry and the 2-second handler get one delivery only

After retry 3 we saw nothing more for that event.

The headers on a retry

This is the full header set of the first retry for the 5-second handler, minus the signature:

{
  "User-Agent": "Slackbot 1.0 (+https://api.slack.com/robots)",
  "Content-Type": "application/json",
  "X-Slack-Request-Timestamp": "1790838690",
  "X-Slack-Retry-Num": "1",
  "X-Slack-Retry-Reason": "http_timeout"
}

The arrival log for that one message, from our handler:

16:11:27.935  Ev0C5FB2K75M  retry=None  reason=None
16:11:31.403  Ev0C5FB2K75M  retry=1  reason=http_timeout
16:12:34.211  Ev0C5FB2K75M  retry=2  reason=http_timeout
16:17:37.381  Ev0C5FB2K75M  retry=3  reason=http_timeout

The first retry came 3.5 s after the first delivery started, which is Slack's timeout plus the network. The second came about 63 s after that, and the third about 5 minutes after the second. The body of each retry was identical to the first, event_id included.

X-Slack-No-Retry only works if Slack gets your response

X-Slack-No-Retry: 1 is a response header. It worked when the handler sent it at once with a 500: Slack did not retry. When the handler slept 4 seconds first, Slack had already given up on the request, never saw the header, and retried 3 times anyway. So the header can tell Slack "do not resend this failure", but it cannot rescue a slow handler.

Never appear "away" on Slack again

Cloud-based. No downloads. Works 24/7 even when your laptop is off.

A handler that stops the retries

The fix is to answer first and work after. This handler returns 200 at once, does the slow part in a thread, and ignores an event_id it has already seen:

import threading, time
from flask import Flask, jsonify, request

app = Flask(__name__)
seen = set()


def handle(event):
    time.sleep(5)  # the slow part: an API call, a database write, an LLM
    print(time.strftime("%H:%M:%S"), "done:", event.get("text"), flush=True)


@app.post("/slack/events")
def slack_events():
    body = request.get_json()
    if body.get("type") == "url_verification":
        return jsonify(challenge=body["challenge"])
    retry = request.headers.get("X-Slack-Retry-Num")
    print(time.strftime("%H:%M:%S"), body["event_id"], "retry:", retry, flush=True)
    if body["event_id"] in seen:
        return "", 200
    seen.add(body["event_id"])
    threading.Thread(target=handle, args=(body["event"],)).start()
    return "", 200


if __name__ == "__main__":
    app.run(port=8791)

We pointed the app at it and sent one message. Its log, with the 5-second job finishing after the 200 had gone out:

16:32:18 Ev0C5FG6BTAB retry: None
16:32:23 done: w1 576 ack-first handler test

Slack delivered the event once. We kept the handler running past the 1-minute mark, when the second retry would have come, and nothing else arrived. The seen set lives in memory, so it is lost on a restart and not shared between processes. In production keep the IDs in Redis or a database table with a short expiry.

Delayed Events

The Event Subscriptions page now has a Delayed Events switch with this text: "If this feature is enabled, we will retry any missed event deliveries slowly over 24 hours. Enable this feature if you want to recover events that your app missed initially during downtime." We turned it on and made the endpoint fail one more event. The first three retries came at the same times as before (0.5 s, 0.9 s, 61.5 s, 362.5 s). We watched for 9 more minutes after retry 3 and no fourth delivery came. Slack's text spreads the slow retries over 24 hours, so a window that short cannot show their timing; we have not measured it.

With Delayed Events off, an event that fails all four deliveries did not come back in our logs. A message history call such as conversations.history can fill the gap for messages. The challenge request that verifies your Request URL in the first place is covered in Slack URL verification; it has the same 3-second limit.

FAQ

Do Socket Mode apps get retries?

Yes. In our Socket Mode test, a message posted while no connection was open reached the app 61 seconds after it was sent, which matches the 1-minute retry here. A Socket Mode app acknowledges each envelope instead of returning HTTP 200.

Does a retry mean the first request failed?

Not always. Our 5-second handler finished its work every time; Slack had only stopped waiting. That is why the work ran 4 times without deduplication.

Always Active

Stop Jiggling Your Mouse.

Join hundreds of remote workers who never worry about their Slack status. Set it up once, stay green forever.

Related Articles

Guide

Slack Interactivity Payload: Real block_actions JSON and response_url Limits

We clicked a button, a static_select and a datepicker in Slack on 1 October 2026 and logged the block_actions payloads our app received. Then we measured response_url: 5 posts, then used_url; it worked at 29:50 and was dead at 30:10.

Slack Green Team
Guide

Slack unfurl_links and unfurl_media: What Each Flag Does, Tested

We posted a web page, a YouTube video and an image with every combination of unfurl_links and unfurl_media on 1 October 2026, then built a custom preview with link_shared and chat.unfurl. The flags do not split the way the names suggest.

Slack Green Team
Guide

Slack Scheduled Message Didn't Send? 5 Cases We Tested

We scheduled five messages for 4:40 PM on 1 October 2026, then archived, left and deleted their channels and deleted a thread parent before the send time. Two sent, three never did, and Slack gave no notice about the three.

Slack Green Team