The Idempotency-Key header (IETF draft) makes a retried POST safe: the server stores the first result per key within a dedup window and replays it for any repeat, giving exactly-once submission. Rate limiting protects the service: over-limit requests get 429 Too Many Requests with Retry-After, and RateLimit headers advertise the remaining quota so clients self-throttle.
The Idempotency-Key header
Message submission is a POST, which is not idempotent, yet networks force clients to retry when a response is lost — creating the classic double-send. The IETF HTTP-API working group's Idempotency-Key draft standardises the fix: the client generates a unique key (a UUID) per logical operation and sends it in the Idempotency-Key request header. The server records the key with the outcome of the first request, and if it sees the same key again it does not execute a second time — it returns the stored result. So a retry after a timeout is guaranteed to be harmless: either the first attempt never landed (and this one executes) or it did (and its result is replayed). The key belongs to the operation, not the connection, so the client reuses the same key across every retry of that one send.
# Generic IETF-draft example (not the BroadNet API, which takes no
# Idempotency-Key header: see /developers/idempotency)
POST /v1/messages HTTP/1.1
Host: api.example
Idempotency-Key: 8f14e45f-ea7b-4c0f-9b2a-1d2c3e4f5a6b
Content-Type: application/json
{ "to": "+15551234567", "text": "..." }
# first call -> 202 Accepted, id 01HZY8Q3M6 (executed, stored)
# same key -> 202 Accepted, id 01HZY8Q3M6 (replayed, not re-sent)Dedup window, response replay and conflicts
An idempotency key is not remembered forever; the server holds it for a dedup window (commonly 24 hours) long enough to cover any realistic retry, then expires it. Within that window a repeat of the key replays the stored status code and body verbatim — the same 202 and the same message id — so the client sees exactly what it would have seen had the first response arrived. Two edge cases need explicit handling. If a key is reused with a different request body, that is a client bug and the server returns 409 Conflict rather than silently sending the wrong thing. If a retry arrives while the first request is still in flight, the server returns a conflict or 'in progress' rather than starting a second execution, so concurrency cannot defeat the guarantee.
| Situation | Server behaviour | Status |
|---|---|---|
| New key | Execute, store key + result | Normal (e.g. 202) |
| Repeat key, same body | Replay stored response, do not re-execute | Original status |
| Repeat key, different body | Reject — key bound to first payload | 409 Conflict |
| Repeat while first in flight | Do not start a second execution | 409 / in-progress |
| Key expired (past window) | Treated as new | Normal |
Rate limiting and 429
A shared messaging platform protects itself and other tenants by capping how fast any client may call, and it signals a breach with a specific status. RFC 9110 and RFC 6585 define 429 Too Many Requests for exactly this: the client has sent too many requests in a given time. A well-behaved server pairs the 429 with a Retry-After header telling the client how long to wait before trying again (either a delay in seconds or an HTTP date). The client's obligation is to honour it — stop, wait the indicated interval, then resume — rather than retrying immediately, which only deepens the overload. A 429 is transient and safe to retry, unlike a 4xx that indicates a malformed request.
HTTP/1.1 429 Too Many Requests
Retry-After: 2
RateLimit: limit=100, remaining=0, reset=2
RateLimit-Policy: 100;w=1
Content-Type: application/problem+json
{ "type": "https://example/problems/rate-limited",
"title": "Too Many Requests", "status": 429 }RateLimit and RateLimit-Policy headers
Reacting to a 429 after the fact is wasteful; the IETF RateLimit-headers draft lets a client see its budget before it exceeds it. On ordinary responses the server advertises the current quota with a RateLimit header carrying the limit, the remaining allowance and the seconds until the window resets, and a RateLimit-Policy header describing the policy itself (for example 100 requests per 1-second window). A client that reads these can pace itself — slowing as remaining approaches zero — and never trigger a 429 at all. This turns rate limiting from a punitive after-the-fact rejection into a cooperative signal, which is the whole point of standardising the header names so any client library can consume them.
| Header | Carries | Example |
|---|---|---|
| 429 Too Many Requests | The over-limit status | status line |
| Retry-After | How long to wait before retrying | Retry-After: 2 |
| RateLimit | limit / remaining / reset for the window | limit=100, remaining=17, reset=1 |
| RateLimit-Policy | The quota policy in force | 100;w=1 (100 per 1s) |
Token bucket and how limits are enforced
The token-bucket algorithm is the usual mechanism behind these headers because it caps the sustained rate while tolerating short bursts. A bucket holds up to a fixed number of tokens and refills at a steady rate; each request removes one token, and a request that finds the bucket empty is rejected with 429. The bucket size sets the burst a client may spend at once and the refill rate sets the long-run ceiling, so a client can send a quick burst after an idle period but cannot exceed the average. The RateLimit remaining value is effectively the token count, and reset is when the bucket will have refilled — which is exactly the information a paced client needs. This HTTP-layer flow control is the REST analogue of SMPP window sizing and enquire_link throttling on the binary session.
Client backpressure
The client half of good rate-limit behaviour is backpressure: shaping outbound load so it stays within the advertised budget instead of firing everything and absorbing 429s. In practice that means a client-side limiter (a token bucket mirroring the server's), reading the RateLimit headers to adjust pace, and on a 429 backing off for the Retry-After interval with jitter before resuming. For a bulk send this means metering submissions to the sustained ceiling rather than dumping them, so the platform stays healthy and the client's own latency stays predictable. BroadNet documents its concrete idempotency and rate-limit behaviour for developers.
Frequently asked
What does the Idempotency-Key header do?
It makes a retried POST safe. The client sends a unique key per logical operation; the server executes the first request with that key and stores the result, then replays the stored result for any repeat of the same key within a dedup window. So a retry after a lost response either executes once or replays — never double-sends. It is specified in an IETF HTTP-API working-group draft.
How long does the server remember an idempotency key?
For a dedup window — commonly around 24 hours — long enough to cover any realistic retry, after which the key expires and a reuse is treated as new. Within the window, a repeat of the key replays the original status code and body verbatim rather than re-executing.
What happens if I reuse an idempotency key with a different body?
The server rejects it with 409 Conflict. The key is bound to the payload of the first request, so reusing it with a different body is treated as a client error rather than silently sending the wrong message. Reuse the same key only for retries of the identical operation.
What should a client do when it receives a 429?
Stop and honour the Retry-After header — wait the indicated seconds (or until the given date), then resume — rather than retrying immediately, which only deepens the overload. A 429 is transient and safe to retry once the wait has elapsed, ideally with a little jitter so many clients do not resume in lockstep.
What are the RateLimit and RateLimit-Policy headers for?
They let a client see its quota before exceeding it. RateLimit carries the limit, the remaining allowance and the reset time for the current window; RateLimit-Policy describes the policy (for example 100 requests per second). A client that reads them can pace itself and avoid triggering a 429 at all. Both are defined in an IETF draft.
How does a token bucket relate to the RateLimit headers?
The token bucket is the enforcement mechanism: a bucket of a fixed size refills at a steady rate, each request spends a token, and an empty bucket yields a 429. The RateLimit remaining value is essentially the current token count and reset is when the bucket refills — so a client reading those headers knows exactly how much burst and sustained rate it has left.
Source specifications
The published standards this page is grounded in. Opens in a new tab.