Reliability
The retry policy
Section titled “The retry policy”One attempt plus three retries by default — --retries changes the count.
| Response | Treatment |
|---|---|
200 with ok: true |
delivered |
200 with ok: false |
retried; Telegram’s description is kept as the error |
429 |
retried after the server’s requested delay |
5xx |
retried with exponential backoff |
| network failure, timeout, DNS | retried with exponential backoff, reported as HTTP 0 |
any other 4xx |
not retried |
A 4xx is terminal on purpose. A revoked token, a chat the bot was removed from, or a
message Telegram cannot parse will answer identically for as long as you keep asking; the
only thing retrying adds is delay before you see the real problem.
Backoff
Section titled “Backoff”1s, 2s, 4s, plus up to 50% jitter.
The jitter is not decoration. When a fleet of jobs notifies the same chat and hits the rate limit together, a fixed schedule makes all of them retry on the same second and trigger the limit again. Spreading them out is the difference between recovering and oscillating.
Rate limits
Section titled “Rate limits”On a 429, notiflow waits for parameters.retry_after from the body; failing that, the
Retry-After response header; failing that, one second. Proxies and self-hosted Bot API
servers tend to send the header rather than the body field, which is why both are read.
The delay is capped at --max-retry-after (60s by default). Telegram occasionally asks for
minutes, and a workflow step that sits idle for that long is usually worse than a missed
notification.
Timeouts
Section titled “Timeouts”--connect-timeout (5s) bounds the TCP and TLS handshake; --timeout (15s) bounds the
whole request. Exceeding either counts as a network failure and follows the backoff path.
Failure and your job’s result
Section titled “Failure and your job’s result”By default a failed notification exits 0. Your build’s result is the thing people act on, and overwriting it because Telegram had a bad minute makes the signal worse, not better.
fail_on_error: true (or --fail-on-error) inverts that when delivery genuinely is part
of the contract. The exit code becomes 1 and the error output carries the reason.
Exit codes
Section titled “Exit codes”| Code | Name | Meaning |
|---|---|---|
| 0 | OK |
Message delivered, or skipped by notify_on, or fail_on_error=false |
| 1 | SEND_FAILED |
Telegram never accepted the message and fail_on_error=true |
| 2 | USAGE |
Command-line parsing failed (emitted by clap) |
| 10 | MISSING_REQUIRED_INPUT |
bot_token, chat_id or status is empty |
| 11 | INVALID_CHAT_ID |
chat_id is not an integer or @username (a comma is rejected in v2) |
| 12 | INVALID_STATUS |
status is not success |
| 13 | INVALID_PARSE_MODE |
parse_mode is not MarkdownV2 |
| 14 | INVALID_NOTIFY_ON |
notify_on lists a value outside the status set |
| 15 | INVALID_THREAD_ID |
message_thread_id is not a non-negative integer |
| 16 | INVALID_EDIT_MESSAGE_ID |
edit_message_id is not a positive integer |
| 17 | CONFIG_ERROR |
Config file unreadable, malformed, or the profile does not exist |
| 18 | INVALID_ARGUMENT |
Argument is well-formed but unusable (bad api_base, zero timeout) |
| 20 | RESERVED |
v1 UNSUPPORTED_BASH — never emitted by v2 |
| 22 | RESERVED |
v1 MISSING_DEPENDENCY — never emitted by v2 |
| 30 | IO_ERROR |
Reading stdin or a message file failed, or an output file is not writable |
Codes 10–16 are inherited verbatim from notiflow v1, so a workflow that branches on them
keeps working across the upgrade. 20 and 22 belonged to the bash runtime — a static binary
has no bash to be too old and no curl to be missing — and are permanently reserved rather
than reused.
Diagnosing a failure
Section titled “Diagnosing a failure”Start with the error output or the JSON report: when Telegram gave a reason, that is the
reason verbatim.
| Symptom | Usual cause |
|---|---|
401 Unauthorized |
wrong or revoked token — check with notiflow whoami |
400 Bad Request: chat not found |
wrong chat_id, or the bot was never added to the chat |
403 Forbidden |
the bot is in the chat but not allowed to post; for a channel it needs to be an administrator |
400 ... can't parse entities |
verbatim message with markup in it — see Parse modes |
429 on every attempt |
too many messages to the same chat; raise --max-retry-after or send fewer |
| HTTP 0 | never reached the server — network, DNS, or a proxy on the runner |
-v adds retry decisions to stderr; -vv adds debug detail. Neither can print the token:
everything notiflow writes goes through a redaction filter first.