# Safe retries

Retry the right failures, the right way.

> **In a nutshell:** Retry 429 and 5xx with exponential backoff, and network failures on sends with the same reference. Never retry other 4xx errors - fix the request instead.

## What to retry

| Outcome | Retry? | How |
|---|---|---|
| `2xx` | - | Done |
| Timeout / connection reset | Yes | Same request, same `reference` |
| `429 rate_limited` | Yes | After the `Retry-After` seconds |
| `500 server_error` | Yes | Exponential backoff |
| `409 not_connected` | After the business reconnects | Offer a connect link first |
| `422 template_not_approved` | After approval | Wait for a `template.status` callback |
| Other `4xx` | No | Fix the request |

## Backoff

Wait longer after each failure and add jitter, for example 1 s, 2 s, 4 s, 8 s... up
to a few minutes, then give up and alert. Run retries in a background queue, never
in a user's web request.

```php
// A Laravel job: retried by the queue with backoff; the reference makes it safe.
class SendWhatsAppNotification implements ShouldQueue
{
    public $tries = 6;
    public $backoff = [5, 30, 120, 600, 1800];

    public function handle(): void
    {
        $response = Http::withToken(config('services.paalchat.token'))->acceptJson()->timeout(20)
            ->post("https://whatsapp.paaltech.org/api/v1/businesses/{$this->tenant}/messages", $this->payload);

        if ($response->status() === 429 || $response->serverError()) {
            $this->release((int) $response->header('Retry-After') ?: 60);
            return;
        }

        $response->throw(); // other 4xx: fail and surface the error code
    }
}
```

```javascript
async function sendWithRetry(body, attempt = 0) {
  try {
    const r = await fetch(`${BASE}/businesses/presec/messages`, { method: 'POST', headers: HEADERS, body: JSON.stringify(body) });
    if (r.status === 429 || r.status >= 500) throw Object.assign(new Error('retry'), { wait: Number(r.headers.get('Retry-After')) || 0 });
    return await r.json();
  } catch (e) {
    if (attempt >= 5) throw e;
    const wait = e.wait || Math.min(2 ** attempt, 300) + Math.random();
    await new Promise((ok) => setTimeout(ok, wait * 1000));
    return sendWithRetry(body, attempt + 1); // same body, same reference
  }
}
```

```python
import random, time
def send_with_retry(body, attempts=6):
    for attempt in range(attempts):
        try:
            r = requests.post(f"{BASE}/businesses/presec/messages", headers=HEADERS, json=body, timeout=20)
            if r.status_code != 429 and r.status_code < 500:
                return r.json()
            wait = int(r.headers.get("Retry-After", 0)) or min(2 ** attempt, 300)
        except requests.RequestException:
            wait = min(2 ** attempt, 300)
        time.sleep(wait + random.random())
    raise RuntimeError("PaalChat unavailable")
```

## Your callback endpoint

PaalChat retries callbacks too. Make your endpoint idempotent (see
[Idempotency](/docs/idempotency)) and answer `2xx` only after the event is safely
recorded.
