Retry & Backoff
Automatic retries with exponential backoff for transient failures.
Transient failures are the norm for notification APIs — network blips, upstream 5xx, connection resets. go-notification retries these automatically with exponential backoff.
Defaults
notification.New(notification.Config{
MaxRetries: 3, // retries AFTER the first attempt → 4 attempts total
RetryDelay: 1 * time.Second, // base delay for exponential backoff
})These are the only two retry knobs. Backoff is computed as RetryDelay * 2^attempt, capped at 60 seconds, with up to 25% jitter added to avoid retry storms.
Default schedule (RetryDelay = 1s, MaxRetries = 3):
- Attempt 1 — fire immediately
- Retry 1 — after ~1s (+ jitter)
- Retry 2 — after ~2s (+ jitter)
- Retry 3 — after ~4s (+ jitter)
- Give up → call
OnError
MaxRetries: 0 is treated as the default (3). To disable retries entirely,
set a negative value (e.g. MaxRetries: -1). RetryDelay <= 0 defaults to 1s.
What gets retried
By default every error is retried until MaxRetries is reached. A driver opts a specific failure out of retry by returning an error that wraps middleware.ErrPermanent:
import "github.com/gopackx/go-notification/middleware"
// Inside a driver's Send:
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
return fmt.Errorf("provider rejected (%d): %w", resp.StatusCode, middleware.ErrPermanent)
}
return fmt.Errorf("provider upstream %d", resp.StatusCode) // retryableSo the convention for HTTP drivers is: 4xx client errors (bad API key, bad recipient, invalid payload) wrap ErrPermanent and stop immediately; network errors and 5xx are left plain and get retried. The built-in HTTP drivers still retry 408, 425 and 429, since those are transient by definition.
SMTP reads the other way round
SMTP reply codes don't follow the HTTP convention. The SMTP driver treats 5xx replies (mailbox unavailable, auth rejected) as permanent and 4xx replies (service busy, greylisting) as transient, so 4xx is retried and 5xx is not. See SMTP.
Errors that are never retried
Three of the library's own sentinel errors are never retried when a driver's Send returns them, because retrying cannot fix them:
ErrNoRoute— the notifiable has no address for this channel. The channel is skipped silently (debug log only; noOnError). The same happens earlier, before any attempt, whenRouteNotificationForreturns"".ErrInvalidMessage— the driver received a message type it doesn't handle (for example a*chat.Messagepassed to a mail driver).OnErrorfires once, and the error message ends with: permanent errorbecause the notifier wraps it inmiddleware.ErrPermanent.errors.Is(err, notification.ErrInvalidMessage)still matches.ErrNoFormatter— the notification has noTo<Channel>()method for this channel. Skipped with a debug log.
Context cancellation
A cancelled context stops the retry loop early. In async mode (the default) the worker runs on context.WithoutCancel(ctx), so the caller's cancellation does not reach it — only the driver's own timeouts apply. In sync mode (Async: false or notification.Sync()) the caller's ctx is used directly and cancelling it abandons the remaining retries. See Context and cancellation.
Jitter
Exponential backoff without jitter causes retry storms — every client hits the upstream at the same instant after a blip. The built-in backoff adds up to 25% jitter on each wait. This is automatic and not configurable.
Observability
Retries themselves aren't surfaced via a per-attempt callback. To observe failures, use OnError (fired once, after all retries are exhausted) and OnSuccess:
notification.New(notification.Config{
MaxRetries: 5,
OnError: func(ctx context.Context, n notification.Notifiable, channel string, err error) {
failureCounter.Add(1)
slog.Error("delivery exhausted retries", "channel", channel, "id", n.GetID(), "err", err)
},
})Sustained OnError spikes mean something upstream is degrading — investigate rather than just absorbing.
Pairs with rate limiting
Retry handles failures you didn't prevent; rate limiting prevents many of them (avoiding 429s in the first place). The two features pair well.