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.
behavior | Effect |
|---|---|
delay | The 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. |
skip | Dropped; 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:
- subscription still opted in and not invalidated (else drop silently),
- frequency cap (unless bypassed),
- quiet hours (unless bypassed),
notBeforefrom local-time scheduling was already applied as the job's delay,- 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.