Skip to main content

Delivery rules

App-wide rules that protect subscribers from too many or badly-timed pushes. They are configured in Dashboard › Delivery rules and stored on the app as:

{
"frequencyCap": { "max": 3, "windowHours": 24 },
"quietHours": { "start": "22:00", "end": "08:00", "behavior": "delay" },
"defaultTimezone": "Europe/Istanbul"
}

Where they run

In the worker, for every push, at the moment it is about to be sent. Not at fan-out. So:

  • A campaign fanned out at 21:59 whose pushes reach the worker at 22:00 obeys quiet hours.
  • A rule you change while a campaign is in flight applies to the pushes still in the queue.
  • The frequency cap counts across every campaign and transactional send, in send order.

Frequency cap

max pushes per user in a rolling windowHours window, counted across all of the user's devices.

Implementation: a Redis counter fc:<appId>:<userId>INCR, with EXPIRE set on the first hit. A push that would exceed max is not sent; the worker writes a skipped event with meta.reason = "frequency_cap", increments stats.skipped, and emits notification.skipped to webhooks.

Two devices, one user, cap 3/24h: after three pushes to either device, both are quiet until the window rolls.

Quiet hours

A local-time window, evaluated in the subscriber's timezone → the app's defaultTimezone → UTC. start > end spans midnight.

behaviorEffect
delayThe push is re-queued with a delay until the window ends (in that user's zone). A deferrals counter rides on the job; after 3 deferrals it is skipped instead — a device stuck in a bad timezone must not accumulate work forever.
skipDropped; skipped event with meta.reason = "quiet_hours".

Deferred pushes are not skips: they show up as sent later, and the hourly chart shows the morning bump.

Bypass

Per campaign or transactional send:

{ "bypass": { "frequencyCap": true, "quietHours": true } }

The wizard's Advanced section has the two checkboxes. Use it for pushes the user asked for or needs now — a 2FA code, a password reset, "your ride is here" — and for nothing promotional. Bypassed pushes still count toward the frequency cap for later pushes.

Order of checks

For each push job the worker evaluates, in order:

  1. subscription still opted in and not invalidated (else drop silently),
  2. frequency cap (unless bypassed),
  3. quiet hours (unless bypassed),
  4. notBefore from local-time scheduling was already applied as the job's delay,
  5. personalise, transport, event.

Reporting

The campaign report's Skip reasons table counts quiet_hours and frequency_cap. The Sent tile's subtitle says how many were held back in total.