---
title: Delivery · BuzzKit
description: A durable queue, progressive retries, and a ledger of every attempt to every device.
canonical: https://buzzkit.dev/features/delivery
last-updated: 2026-09-02
---

# Every attempt, accounted for. Queued, retried, recorded.

One send fans out to every reachable device through a durable queue. Each attempt goes to Apple or Firebase with your credentials, is retried with backoff when that makes sense, and is written to a ledger with the request, the response and the latency.

## Retries that respect the provider

Transient failures retry at 5 seconds, 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour and 2 hours after the first attempt, each with jitter. Retry-After is honored, rate limits and timeouts carry a 60 second floor, and a lost job is re-driven by the reconciliation cron.

```
GET /v1/deliveries/dlv_8h2k
{
  "status": "retrying",
  "attempts": 3,
  "lastErrorCode": "rate_limited",
  "nextAttemptAt": "2026-09-01T09:12:04Z",
  "externalId": "user_42",
  "platform": "ios"
}
```

## One error language for every provider

Providers classify their native reasons into a shared taxonomy, and policy lives in the core. A dead token flips the subscription to invalid, rate limits and outages retry, bad credentials fail at once, and a subscription muted mid-flight fails as unsubscribed.

```
GET /v1/deliveries/dlv_8h2k/attempts
{
  "data": [
    {
      "attempt": 1,
      "outcome": "retrying",
      "errorCode": "rate_limited",
      "providerStatus": 429,
      "latencyMs": 412
    },
    {
      "attempt": 2,
      "outcome": "sent",
      "providerStatus": 200,
      "latencyMs": 142
    }
  ]
}
```

## Counts you can trust

While a message is processing, counters advance per batch. Completion is derived, never counted: once nothing is pending or retrying, every counter is recounted from the deliveries and written exactly.

```
GET /v1/messages/msg_7g2h
{
  "status": "completed",
  "counts": {
    "total": 2418,
    "sent": 2412,
    "failed": 3,
    "invalid": 3
  },
  "completedAt": "2026-09-01T09:04:12Z"
}
```

## Capabilities

- **Durable fan-out.** Pages of 500 subscriptions chain themselves and resume from a cursor.
- **Idempotent sends.** An idempotency key makes identical requests one message.
- **Dead tokens cleaned up.** An APNs 410 or FCM unregistered flips the subscription to invalid.
- **The attempt ledger.** Request, response, provider reason and latency on every attempt.
- **Send policy.** Quiet hours and daily caps per tenant and topic.
- **Dead-letter queue.** Jobs that crash repeatedly land somewhere visible.

## Questions

### What happens when a push fails?

A temporary failure, such as a rate limit or an outage, retries with backoff for about four hours. A dead token marks the device invalid, and a bad credential fails at once. Every attempt stays in the ledger.

### How do I know a push actually reached the device?

Sent means the provider accepted it, which is the most a push provider confirms. The iOS SDK also tracks $notification.delivered and $notification.opened on the subscriber timeline.

### Can a retry send a notification to someone who unsubscribed in the meantime?

No. Every attempt checks the subscription first and fails as unsubscribed if it was muted, removed or invalidated after fan-out.

## Related

- [Sending](https://buzzkit.dev/features/sending.md): One POST sends to a subscriber, a topic or a segment and lands on every device.
- [Scheduling](https://buzzkit.dev/features/scheduling.md): Hold a message for a moment, in one timezone or in every subscriber’s own.
- [iOS SDK](https://buzzkit.dev/features/ios-sdk.md): Registration, identity, events, action buttons and a notification settings screen in one Swift package.

## Start

- [Start sending](https://buzzkit.dev/dashboard)
- [API Reference](https://docs.buzzkit.dev)
- [BuzzKit on GitHub](https://github.com/buzzkit-dev/buzzkit)
