Transactional vs Marketing Push Notifications

An order confirmation and a weekend sale share an API and nothing else. This reference sets out the operational split — queues, headers, caps, consent basis and VAPID identity — and shows what breaks when both classes run through one pipeline.

The Split at a Glance

Dimension Transactional Marketing
Trigger A user action or account event A schedule or a segment rule
Queue Dedicated low-latency queue, small concurrency Batch queue, high concurrency, backpressure-aware
Urgency high (or normal for a shipping update) low, occasionally normal
TTL Minutes to a few hours — the fact expires Hours to a day — the offer expires
Topic Per-entity, e.g. one per order Per-campaign, so a stale send collapses
Frequency cap Exempt Enforced
Quiet hours Exempt (with judgement) Enforced
GDPR basis Contract, or legitimate interest Consent
Opt-out Only by closing the account or revoking permission Granular, honoured immediately
VAPID subject An operational contact address A marketing contact address
On failure Page someone; fall back to email or SMS Log and drop

Everything below is an elaboration of one rule: the two classes must not share a code path that can decide to drop a message.

Two Lanes, One Push Service

Both classes eventually make the same HTTP request to the same push service. What differs is everything upstream of that request: which queue holds the job, which gates it passes, and what headers it carries.

Separate transactional and marketing pipelines The transactional lane runs from the order service through a dedicated queue with high urgency and a short TTL. The marketing lane runs from the campaign scheduler through a batch queue and a frequency-cap and quiet-hours gate. Both converge on the push service and then the device. Two lanes, one push service TRANSACTIONAL Order service event-driven queue push.tx low latency, retried Urgency: high TTL 3600, no gates VAPID subject mailto:ops — Topic per order id MARKETING Scheduler segment driven queue push.mkt batched, throttled Cap + quiet hours may drop the job VAPID subject mailto:marketing — Topic per campaign id Push service RFC 8030 Device service worker only the marketing lane owns a gate that is allowed to discard a message separate queues also stop a campaign backlog from delaying a receipt
Separate queues buy two things: independent latency budgets and a physical guarantee that marketing gates cannot reach transactional jobs.

The queues need genuinely different characteristics. A transactional queue is shallow, has aggressive per-job retry with jitter, and alerts when depth exceeds a handful of seconds’ worth of work. A marketing queue is deep by design, sheds load gracefully, and is expected to take minutes to drain — the batching and concurrency choices are covered in message batching and throughput optimization.

Urgency, TTL and Topic

RFC 8030 gives you three request headers that encode intent to the push service, and the correct values differ sharply by class.

Urgency tells the push service how much it may delay delivery to save the device’s battery. A device on a metered connection or in a low-power state may hold low and very-low messages until it next wakes. Use high for a two-factor prompt or a payment failure; normal for a shipping update; low for a campaign. Marking everything high does not make delivery faster in aggregate — it removes the signal the push service uses to protect the user’s battery, and it is the sort of thing providers notice.

TTL is how long the push service should keep trying before discarding. The right value is the moment the message stops being true. A “your code is 402 913” push has a TTL of five minutes because the code expires. A flash-sale push has a TTL of a few hours because the sale ends. A digest has a TTL of a day. The failure mode of a too-long TTL — a message arriving long after it made sense — is covered in TTL 0 vs TTL 86400 delivery guarantees.

Topic makes the push service replace an undelivered message with a newer one carrying the same topic. Scope it per entity for transactional messages — one topic per order, so “shipped” replaces “packed” if the device was offline — and per campaign for marketing, so a subscriber who was offline all day gets today’s digest and not last Tuesday’s.

Payload rules are identical for both classes: the ciphertext is capped at 4 KB and must use the aes128gcm content encoding from RFC 8291, so both lanes send ids and a URL rather than rendered content.

import webpush from 'web-push';

const PROFILES = {
  transactional: { urgency: 'high', ttl: 3600, subject: 'mailto:ops@example.com' },
  marketing:     { urgency: 'low',  ttl: 43200, subject: 'mailto:marketing@example.com' }
};

export async function send(kind, sub, payload, topic) {
  const p = PROFILES[kind];
  if (!p) throw new Error(`unknown notification class: ${kind}`);
  webpush.setVapidDetails(p.subject, process.env.VAPID_PUBLIC_KEY, process.env.VAPID_PRIVATE_KEY);
  return webpush.sendNotification(sub, JSON.stringify(payload), {
    TTL: p.ttl,
    urgency: p.urgency,
    topic                                   // 'order-9f2a' or 'camp-spring-sale'
  });
}

Note that the VAPID subject differs while the key pair stays the same. The subject is the contact a push service operator uses when something is wrong with your traffic, so pointing marketing volume at a marketing owner and transactional volume at on-call is straightforwardly useful. If you genuinely want separate key pairs — some teams do, to isolate reputation — remember that each key pair implies a separate subscription, so a subscriber must subscribe twice; the mechanics and the migration cost are covered in VAPID key generation and rotation. Whichever route you take, the keys come from process.env.VAPID_PUBLIC_KEY and process.env.VAPID_PRIVATE_KEY and are never committed.

What Goes Wrong in a Single Pipeline

The classic incident: a subscriber gets three campaign notifications during the day, hits the daily frequency cap, then places an order in the evening. The receipt enters the same queue, hits the same cap check, and is silently discarded. Support hears about it a week later as “I never got my confirmation”.

A shared pipeline drops an order receipt Three marketing notifications during the day consume a cap of three, and the order receipt arriving at 19:20 is discarded by the same frequency-cap gate because the pipeline cannot tell the classes apart. One shared pipeline: the receipt is the message that loses 19:20 order receipt dropped by the cap 10:00 campaign cap 1 of 3 14:00 digest cap 2 of 3 18:00 win-back cap 3 of 3 09:00 12:00 15:00 18:00 21:00 daily cap budget: 3 of 3 consumed by 18:00 the gate cannot tell a receipt from a campaign
Nothing here is a bug in the cap. The bug is that a receipt was ever routed through it.

The same shared-pipeline shape produces three more failures worth naming. A campaign backlog of 400,000 jobs sits in front of the receipt, so it arrives forty minutes late. Quiet hours suppress a fraud alert at 23:40. A subscriber who opted out of marketing stops receiving password-reset notifications, because one boolean governed both.

Every one of these is fixed by classification at the entry point rather than by conditionals deeper in the stack. Make the class a required, non-defaulting argument, and make the cap check physically unreachable from the transactional path.

// entry point — the class is required and drives routing
const CLASSES = new Set(['transactional', 'marketing']);

export async function enqueue({ kind, subscriberId, payload, topic }) {
  if (!CLASSES.has(kind)) throw new Error('notification class is required');

  if (kind === 'transactional') {
    return queues.tx.add('send', { subscriberId, payload, topic }, {
      attempts: 8, backoff: { type: 'exponential', delay: 500 }, priority: 1
    });
  }

  const allowed = await capGate.check(subscriberId);          // marketing only
  if (!allowed.ok) {
    metrics.increment('push.marketing.suppressed', { reason: allowed.reason });
    return null;
  }
  return queues.mkt.add('send', { subscriberId, payload, topic }, {
    attempts: 3, backoff: { type: 'exponential', delay: 5000 }, priority: 10
  });
}

Cap design itself — window, counter storage, per-category budgets — is covered in setting push notification frequency caps. The point here is only where the gate is allowed to live.

Under GDPR the two classes rest on different legal bases, and the difference is not cosmetic — it determines what an opt-out must do.

Marketing push relies on consent: freely given, specific, informed, unambiguous, and as easy to withdraw as it was to give. That means a granular preference surface, an immediate effect on withdrawal, and a stored record of when and how consent was captured. The permission grant in the browser is not, by itself, that record; it proves the user allowed notifications, not that they agreed to marketing.

Transactional push relies on contract (performing a contract the user is party to) or on legitimate interest for things like security alerts. A shipping notification is part of delivering the order; it is not marketing and does not need marketing consent. It also, critically, must not be suppressed by a marketing opt-out — withholding an order confirmation because someone unsubscribed from a newsletter is a service failure dressed up as compliance.

Choosing the legal basis for a push notification A message required to fulfil an order with no promotion is transactional under contract and exempt from caps. A message that fulfils an action but also carries an offer becomes marketing. A purely promotional message is marketing under consent. What is this notification for? answer before you choose a queue Required to fulfil an order or an account action, with no promotional content Fulfils an action but also carries an offer, upsell or cross-sell Campaign, digest, win-back or product announcement with no account event TRANSACTIONAL basis: contract exempt from caps MARKETING the offer decides it split it into two sends MARKETING basis: consent opt-out honoured at once a receipt with a discount code attached is a marketing message wearing a receipt when in doubt, classify as marketing — the cost of guessing wrong runs one way
The middle branch is where most teams get into trouble: bolting an offer onto a receipt reclassifies the whole message.

Practical consequences for the preference surface, which should follow the patterns in opt-out and preference centres:

  • Present marketing categories as individually togglable, and show transactional notifications as a separate, non-togglable list with an explanation of why.
  • Store the class on every send record. When someone exercises a data-subject request you need to say which messages were sent on which basis.
  • On marketing withdrawal, suppress marketing immediately and leave the push subscription itself intact. Only a full account closure or a browser-level permission revocation should end transactional delivery.
  • Keep the consent timestamp, the wording shown, and the version of that wording. “The user clicked allow” is not a defensible record on its own.

Gotchas & Edge Cases

  • A receipt with a promo code is marketing. If you want to upsell, send the receipt transactionally and the offer as a separate marketing send. Merging them puts the receipt behind the consent gate.
  • Do not exempt transactional from quiet hours reflexively. A 03:00 “your subscription renewed” push is technically transactional and still a bad notification. Exempt genuinely urgent classes; queue the rest until morning.
  • Alerting differs. A failed transactional send needs a fallback channel and a page. A failed marketing send needs a counter. Wiring both to the same alert policy produces either noise or silence.
  • Watch the Topic collision. Two order updates for different orders must not share a topic, or one silently replaces the other in the push service queue.
  • Split the dashboards. Blending both classes into one click-through number is meaningless — transactional click-through is structurally several times higher and will mask everything happening to campaigns.
  • Breaking-news alerts are their own class. They are neither contract-based nor ordinary marketing; they need their own consent category and their own fan-out design, as described in breaking-news push alert architecture.

FAQ

Do transactional push notifications need marketing consent under GDPR?

No. A notification that is necessary to perform a contract the user entered into, such as an order confirmation or a shipping update, rests on the contract basis rather than consent, and a security alert can rest on legitimate interest. That also means a marketing opt-out must not suppress it. The moment you add a promotional element the message becomes marketing and needs consent.

Should transactional and marketing push use different VAPID keys?

Different VAPID subjects are cheap and useful, because the subject is the contact a push service operator reaches when your traffic causes problems. Different key pairs are a much bigger change: the application server key is part of the subscription, so a separate pair means each subscriber has to subscribe twice and you have two subscription lifecycles to maintain.

Why did my order confirmation push never arrive?

The most common cause is a shared pipeline where a marketing frequency cap or a quiet-hours rule evaluated the receipt and discarded it. Route transactional sends through a separate queue that no cap gate can reach, log every suppression with a reason, and check that log first when a receipt goes missing.