Skip to content

Sending pushes

ctx.push.send: who a push reaches, how a large send is split, what happens to each device and the limits that always apply.

A function that declares push in its function.yaml has ctx.push.send. The call answers at once; backd’s workers deliver.

export default async function (ctx) {
  const r = await ctx.push.send({
    user: order.owner,                  // or users: [...], or audience (below)
    kind: "order-shipped",              // a template of the realm, or title + body (raw)
    data: { order_no: order.no, eta: "tomorrow" },
    group: `order-${order.id}`,
    priority: "high",
  });
  // r = { send_id: "ps_…", users: 1, jobs: 1, queued: 1, limited: 0, no_devices: 0 }
}
# function.yaml
push: true                              # or push: { kinds: [order-shipped], audience: false }

Who

Give exactly one of:

FieldReaches
userOne user, by id
usersAny number of users, by id. There is no per-call cap
audienceUsers that match a filter, for a function with push: { audience: true }: "all", or an object with roles (holding any of them), email_verified, locale, consent (users who hold that consent), created_before and created_after. Disabled and erased users never match

requires_consent: "marketing" skips the users who don’t hold that consent: they are counted as no_consent in the result and in the send’s record, and they don’t count against their limits. It enforces a withdrawal even when the function’s own code forgets to check, and works with user, users and audience alike.

await ctx.push.send({ audience: "all", title: "New arrivals", body: "…", requires_consent: "marketing" });
// { …, queued: 980, no_consent: 20 }

A consent the realm doesn’t declare is 400 invalid_push_message.

A list of a million ids can’t go through a function call, which is what audience is for: backd pages through the matching users itself, so the audience is fixed batch by batch.

What

kind names a template; without it, title and body are the text (a raw message). The other fields (link, data, badge, sound, group, ttl, priority, silent, platform) are described with the message. data values are strings. The message is rendered in every language of the realm and checked before anything is queued; a function that can’t send gets an error it can show or let end it:

CodeWhen
unknown_push_kindThe kind has no template, or the function didn’t declare it
invalid_push_messageA field is not acceptable (the answer names it)
payload_too_largeA rendering is over a provider’s limit
push_audience_not_declaredAn audience without push: { audience: true }
push_unavailable (503)A secret the providers need is not set
push_not_configured (404)The realm has no push: section

What backd does with it

flowchart LR
    F[ctx.push.send] --> S[send record]
    S --> P[planner: pages an audience]
    P --> B[batch: up to 1,000 users]
    S --> B
    B -->|limits, devices, language| U[delivery unit: up to 100 users]
    U --> W[worker: one request set per provider]
  • A batch applies the limits, resolves each user’s devices (only those whose session is still alive), picks each user’s language and makes delivery units of up to 100 users. A send of up to 1,000 users is planned during the call, so the answer already says how many were limited or had no_devices; larger sends answer null for those and the counts appear on the send as batches run.
  • A delivery unit carries device ids, never tokens: the worker loads and decrypts them when it runs. Each device gets a result:
ResultMeaningEffect
sentThe provider accepted itCounted
invalidThe token is deadThe device is removed
retryThrottled or transientOnly these devices are sent again, after the backoff
failedPermanent for this pushCounted; the device stays

A device the sender gave no result for counts as failed. A failure of the call as a whole (the network, rejected credentials) retries the whole unit under push.delivery.retry; devices already delivered in that attempt may get the push twice. After the last attempt, what is still pending counts as failed.

  • Idempotent planning. If a worker dies mid-batch, the batch runs again from where it was: units have deterministic ids, so none is made twice and nobody is charged twice.

Limits

Per user, push.limits.per_user (5 a minute and 24 a day by default), counted once per push however many devices the user has. Silent pushes count too, and nobody is exempt: there is no urgent flag and no administrator bypass. A user over their limit is skipped and counted as limited; there is no per-call cap, so very large sends are kept in check by batching, these limits and push.delivery.parallelism.

wait

wait: true waits for the totals within the function’s time limit and returns what it has then, with sent, invalid, failed and completed. Don’t use it for large sends.

Edit this page · Last updated 15 Oct 2026