Retries and rate limits
Every channel sends through the same transport, and it does three things you would otherwise write yourself. All three are on by default and all three can be turned off.
Copy this
export default defineEventHandler(async () => {
await telegram.send('Deploy failed', {
retries: 3,
retryDelayMs: 1000,
timeoutMs: 5000,
})
return { ok: true }
})
That runs, and it is the same three options on every channel and every verb.
| Option | Default | What it does |
|---|---|---|
retries | 2 | Extra attempts after the first. 0 turns it off |
retryDelayMs | 500 | Wait before the second attempt, doubled each time after |
timeoutMs | 10000 | Per attempt, not for the whole thing. 0 disables it |
retryOnNetworkError | false | See rule three |
With the defaults, a failing request takes three attempts and waits 500 then 1000 milliseconds in between.
Rule one: not every failure is worth repeating
A 500 is a bad moment. A 404 is a bad url, and trying it again only wastes the time your user is waiting.
| Status | Retried |
|---|---|
| 408, 409, 425, 429 | ✅ |
| 500, 502, 503, 504 | ✅ |
| 400, 401, 403, 404 | ❌ your request is wrong, repeating it changes nothing |
| 2xx | not a failure |
Rule two: what the service asks for beats what we would pick
If the answer carries Retry-After, that wins over the doubling. The service knows when
it will be ready and we do not.
Telegram puts the same number in the body rather than the header:
{ "ok": false, "error_code": 429, "parameters": { "retry_after": 30 } }
So the channel hands its own reading down to the transport, and the transport stays free of any knowledge about Telegram. A very large value is trimmed to 60 seconds, because waiting longer helps nobody.
Rule three: no answer means no retry
This is the one worth knowing.
So a network error is reported rather than repeated. If your case is different, for example an idempotent endpoint of your own, say so:
await webhook.send(body, { retryOnNetworkError: true })
Where a service offers a real guard, use that instead. Mastodon has one:
await mastodon.post(text, { idempotencyKey: deployId })
The same key twice creates no second post, so retrying is safe by construction.
Seeing it rather than believing it
The playground has a card for this. Pick a status, press, and the elapsed time is the proof: nothing counts attempts inside the transport, the clock does it from outside.
3 attempts, waiting 500 + 1000 = 1500ms in between
→ Answered 500 in 1841ms
What will cost you an hour
timeoutMsis per attempt. Three attempts at ten seconds can take thirty, plus the waiting. For a request a user is waiting on, lower it.- A 429 is not always a retry. If the service asks for 30 seconds, the module waits 30 seconds. That is correct and it is also a long time to hold a request open.
- Rate limits are per service, not per module. Telegram allows roughly 30 messages a second globally and about 20 a minute into one group. Nothing here queues for you.
- Do not make a user wait for a notification. If the message is not what they asked for, send it after the response rather than before it.
Read more
- Limits and errors
- ofetch - the fetch wrapper underneath
- MDN: Retry-After
Any webhook
Send and receive webhooks in Nuxt: any url, any payload, Standard Webhooks signing, and one route for GitHub, Stripe, n8n or anything else that speaks HTTP.
Limits and errors
Limits checked before sending, errors that never carry a token, and refusals that say what to do. Safe to log, and useful when they land in your tracker.