How We Reduced Platform Event Delivery Costs by 60% (And You Can Too)

A deep dive into the hidden costs of Salesforce Platform Events and the surprising optimization that saved us 50,000+ delivery allocations per day


TL;DR

  • The Problem: Platform Events deliver to every browser tab separately, causing 200-300% cost multiplication during peak usage
  • Root Cause: Each tab creates a separate CometD subscription - 4 tabs per user = 4x delivery allocation usage
  • Solution: Client-side tab coordination where only ONE tab per user subscribes to Platform Events
  • Results: 50-70% reduction in delivery costs, eliminated allocation limit issues during peak hours
  • Alternative: Consider Salesforce native notifications for async processes (zero Platform Event usage)
  • When to optimize: 50+ users with 3+ tabs each, hitting delivery limits, seeing usage spikes

The 100k Mystery

Picture this: Your Salesforce org is humming along nicely. Users love the real-time notifications you've built with Platform Events. Everything seems perfect.

Then you check your Platform Event delivery allocation and see this:

Monday: 75,000 deliveries ✅ Normal
Tuesday: 75,000 deliveries ✅ Normal  
Wednesday: 225,000 deliveries ❌ WHAT?!

A 3x spike overnight. No new users. No new features. No bulk data imports. Just... chaos.

Sound familiar? You might be suffering from what I call "The Tab Multiplication Problem" - one of the most expensive hidden costs in Salesforce Platform Events.

The Real Culprit: It's Not What You Think

Your first instinct might be to blame runaway batch jobs publishing too many events, users triggering excessive workflow rules, or integration systems gone rogue.

But here's the twist: The problem isn't how many events you publish. It's how many times each event gets delivered.

The Multi-Tab Reality

Here's what was actually happening to us every single day:

The Pattern We Discovered:

  • Users naturally open multiple Salesforce tabs during their workday
  • Case record, Account record, Dashboard, Reports
  • Each tab loads our Lightning component
  • Each component subscribes to Platform Events
  • Every single tab receives every single event

The math is brutal:

Single tab usage: 30 users × 1 subscription × 250 events = 7,500 deliveries
Multi-tab reality: 30 users × 3 tabs × 250 events = 22,500 deliveries

A 200% increase from users simply... having multiple tabs open.

Why we initially blamed "meetings": We noticed spikes during meeting hours (9-11 AM, 1-3 PM) because that's when users prep by opening multiple tabs, then leave everything running. But the real culprit was the multiple tabs, not the meetings themselves.

The Platform Event Delivery Model You Need to Understand

Here's what most developers get wrong about Platform Events:

❌ Common Misconception: "Platform Events are delivered once per user session"

✅ Reality: "Platform Events are delivered once per CometD subscriber"

From the official Salesforce documentation:

"The lightning-emp-api component creates a unique CometD connection for every user session... if there are 5K active customers subscribing to events via the component, the site would consume 50K (5K*10) of the event delivery count"

Each browser tab = separate subscription = separate delivery allocation

This means if the same user has multiple tabs open, you get multiple subscriptions. Multiple subscriptions mean multiplied delivery costs. Since most users keep multiple Salesforce tabs open throughout their workday, you end up with 2-4x the expected delivery allocation usage.

The Solution: Tab Coordination Architecture

Instead of fighting this behavior, we embraced it with a coordination pattern:

Primary/Secondary Tab Pattern

// Only ONE tab per user subscribes to Platform Events
Tab 1 (Primary): Creates Platform Event subscription
                 ↓
               Platform Events
                 ↓
               localStorage
                 ↓
Tab 2,3,4 (Secondary): Poll localStorage for events

Key components:

  1. Atomic Coordination Lock

    // Prevent race conditions when multiple tabs load
    const lockAcquired = await this.acquireCoordinationLock();
    if (lockAcquired) {
      // Become primary tab
    } else {
      // Become secondary tab
    }
    
  2. Cross-Tab Event Sharing

    // Primary tab stores events for secondary tabs
    localStorage.setItem('events', JSON.stringify(eventData));
    
    // Secondary tabs poll for new events
    setInterval(() => {
      this.checkForNewEvents();
    }, 3000);
    
  3. Automatic Failover

    // If primary tab dies, secondary takes over
    if (!this.isPrimaryTabHealthy()) {
      this.becomePrimaryTab();
    }
    

The Implementation: Getting Your Hands Dirty

Here's the core coordination logic that solved our problem:

export default class OptimizedPlatformEventComponent extends LightningElement {
    // Unique tab identifier
    tabId = `tab_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    isPrimaryTab = false;
    
    async connectedCallback() {
        // Random delay prevents simultaneous coordination
        setTimeout(() => {
            this.initializeTabCoordination();
        }, Math.random() * 2000);
    }
    
    async initializeTabCoordination() {
        // Step 1: Try to acquire coordination lock
        const lockAcquired = await this.acquireCoordinationLock();
        
        if (!lockAcquired) {
            this.becomeSecondaryTab();
            return;
        }
        
        // Step 2: Check if healthy primary exists
        const existingPrimary = this.getExistingPrimaryInfo();
        
        if (existingPrimary && this.isPrimaryTabHealthy(existingPrimary)) {
            this.releaseCoordinationLock();
            this.becomeSecondaryTab();
            return;
        }
        
        // Step 3: Become primary
        await this.becomePrimaryTab();
    }
    
    async becomePrimaryTab() {
        this.isPrimaryTab = true;
        
        // Create the ONE Platform Event subscription
        this.subscription = await subscribe(
            '/event/Messaging_Utility_Event__e',
            -1,
            this.handlePlatformEvent.bind(this)
        );
        
        // Start heartbeat for health monitoring
        this.startHeartbeat();
        this.releaseCoordinationLock();
    }
    
    handlePlatformEvent(response) {
        const eventData = response.data.payload;
        
        // Store for secondary tabs
        this.storeEventForSecondaryTabs(eventData);
        
        // Process on primary tab
        this.processEvent(eventData);
    }
}

The Results: 60% Reduction in Delivery Allocation

After implementing tab coordination:

Before the optimization, we were seeing 3 tabs per user × 75,000 events = 225,000 daily deliveries. We'd frequently hit allocation limits during peak usage hours, which led to angry users when notifications stopped working completely.

After implementing tab coordination, we dropped to 1 subscription per user × 75,000 events = 75,000 daily deliveries. That's a 60% reduction in Platform Event costs with zero allocation limit issues, and users still get their notifications on all tabs.

The Gotchas: What We Learned the Hard Way

1. The Browser Storage Maze: Why Simple Solutions Don't Work

Our first instinct: "Just use localStorage to coordinate between tabs!"

❌ What we tried initially:

// Naive approach - check if subscription already exists
connectedCallback() {
    const existingSubscription = localStorage.getItem('platform_event_subscription');
    if (!existingSubscription) {
        // Only subscribe if no existing subscription
        this.subscribeToEvents();
        localStorage.setItem('platform_event_subscription', 'active');
    }
}

The brutal reality: This breaks in unexpected ways depending on HOW users open tabs.

Browser Tab Behavior Matrix

Here's what we discovered through painful testing:

User Action localStorage State sessionStorage State Our Component Behavior
New tab (Ctrl+T) ✅ Shared ❌ New session ✅ Coordination works
Duplicate tab (Ctrl+Shift+K) ✅ Shared ✅ Copied ❌ Both tabs think they're primary
Hard refresh (Ctrl+Shift+R) ✅ Persists ❌ Cleared ❌ Original tab loses coordination
Back/Forward navigation ✅ Persists ✅ Persists ❌ Multiple subscriptions created
Open link in new tab ✅ Shared ❌ New session ✅ Coordination works
Restore closed tab (Ctrl+Shift+T) ✅ Persists ❌ New session ❌ Conflicts with existing tabs

The sessionStorage Trap

❌ sessionStorage approach that failed:

// This seemed logical but broke constantly
connectedCallback() {
    const sessionId = sessionStorage.getItem('tab_session_id');
    if (!sessionId) {
        // Assume this is the primary tab
        this.becomePrimary();
        sessionStorage.setItem('tab_session_id', this.generateId());
    }
}

Why it failed:

  • Tab duplication → Multiple "primary" tabs with different session IDs
  • Navigation within tab → Session persists, but component reinitializes
  • Refresh behavior → Inconsistent across different refresh types

The localStorage Race Condition Nightmare

❌ Simple localStorage coordination that failed:

connectedCallback() {
    const primaryTab = localStorage.getItem('primary_tab_id');
    if (!primaryTab) {
        localStorage.setItem('primary_tab_id', this.tabId);
        this.becomePrimary();
    }
}

What went wrong:

10:00:01.250 - Tab A: Check localStorage → null
10:00:01.251 - Tab B: Check localStorage → null  
10:00:01.252 - Tab A: Set primary_tab_id = 'tab_a'
10:00:01.253 - Tab B: Set primary_tab_id = 'tab_b' (overwrites!)
10:00:01.254 - Both tabs think they're primary

The Solution: Atomic Locking + Verification

After countless failures, we learned that coordination requires:

1. Atomic operations with verification

async acquireCoordinationLock() {
    // Try to acquire lock
    localStorage.setItem(lockKey, JSON.stringify({
        tabId: this.tabId,
        timestamp: Date.now()
    }));
    
    // CRITICAL: Verify we actually got it
    await this.sleep(200);
    const verifyLock = localStorage.getItem(lockKey);
    const currentLock = JSON.parse(verifyLock);
    
    return currentLock.tabId === this.tabId;
}

2. Heartbeat-based health monitoring

// Don't trust localStorage state - verify primary is alive
isPrimaryTabHealthy(primaryInfo) {
    const heartbeat = localStorage.getItem('heartbeat');
    const lastBeat = JSON.parse(heartbeat).timestamp;
    return (Date.now() - lastBeat) < 60000; // 60 second timeout
}

3. Graceful race condition handling

// If verification fails, gracefully become secondary
if (!lockAcquired) {
    console.log('Lost coordination race - becoming secondary');
    this.becomeSecondaryTab();
}

2. localStorage Isn't Shared Across Tabs (Wait, What?)

❌ Wrong assumption:

// This doesn't work across tabs
static sharedSubscription = null;

✅ Correct approach:

// localStorage IS shared across tabs for same domain
localStorage.setItem('subscription_info', data);

3. Race Conditions Are Real

When multiple tabs load simultaneously, you get coordination races. Solution: atomic locking with verification.

// Always verify you actually got the lock
localStorage.setItem(lockKey, JSON.stringify({tabId: this.tabId}));

await this.sleep(200); // Brief delay

const verifyLock = localStorage.getItem(lockKey);
if (JSON.parse(verifyLock).tabId !== this.tabId) {
    // Lost the race - become secondary
    this.becomeSecondaryTab();
}

4. Notification Deduplication Is Essential

Without deduplication, users see the same notification on every tab:

// Check if user already saw this notification
if (this.wasNotificationSeen(eventId)) {
    return; // Skip duplicate
}

// Mark as seen immediately for visible tabs
if (!document.hidden) {
    this.markNotificationAsSeen(eventId);
}

5. Browser Notifications for Background Tabs

Secondary tabs need browser notifications since users might not see toast notifications:

// Show browser notification when appropriate
if (document.hidden || !this.isPrimaryTab) {
    this.showBrowserNotification(eventData);
}

When NOT to Use This Pattern

This optimization isn't always worth the complexity. If you have fewer than 30 concurrent users who typically keep just 1-2 tabs open, or your events are infrequent (less than 50 per day), you probably don't need this yet.

But if you have 50+ concurrent users who commonly have 3+ Salesforce tabs open, you're hitting delivery allocation limits, or you see usage spikes that correlate with peak work hours, then this optimization could save you significant headaches.

The Monitoring Strategy

Track these metrics to know if you need this optimization:

// Monitor subscription multiplication
console.log('Active subscriptions:', subscriptionCount);
console.log('Active user sessions:', userSessionCount);
console.log('Multiplication factor:', subscriptionCount / userSessionCount);

// Multiplication factor > 2.0 = optimization needed

Red flags to watch for:

  • Delivery allocation consistently 2-4x higher than expected
  • High delivery-to-event ratios (> 3:1)
  • Users reporting missing notifications when you hit allocation limits

Alternative Approaches: When Client-Side Coordination Isn't Enough

If the 80% solution isn't sufficient for your use case, here are more robust alternatives that address the fundamental delivery allocation problem.

Approach 1: Server-Side Fan-Out Pattern

Instead of multiple client subscriptions, use a single server-side subscription that fans out to individual users.

Architecture:

Platform Events → Single Heroku/External Service → WebSockets → Individual Browser Tabs

Implementation:

// Heroku Node.js Service
const jsforce = require('jsforce');
const WebSocket = require('ws');

class PlatformEventProxy {
    constructor() {
        this.userConnections = new Map(); // userId -> Set of WebSocket connections
        this.salesforceConnection = new jsforce.Connection();
    }
    
    async start() {
        // Single Platform Event subscription on server
        await this.salesforceConnection.streaming.subscribe(
            '/event/Messaging_Utility_Event__e', 
            (message) => this.fanOutToUsers(message)
        );
        
        // WebSocket server for browser connections
        this.wss = new WebSocket.Server({ port: 8080 });
        this.wss.on('connection', (ws, req) => this.handleUserConnection(ws, req));
    }
    
    fanOutToUsers(platformEvent) {
        const eventData = platformEvent.sobject;
        const targetUserId = eventData.Target_User_Id__c;
        
        if (eventData.Is_Global__c) {
            // Send to all connected users
            this.userConnections.forEach(connections => {
                connections.forEach(ws => this.sendSafely(ws, eventData));
            });
        } else if (targetUserId) {
            // Send to specific user's connections
            const userConnections = this.userConnections.get(targetUserId) || new Set();
            userConnections.forEach(ws => this.sendSafely(ws, eventData));
        }
    }
    
    handleUserConnection(ws, req) {
        const userId = this.extractUserIdFromAuth(req);
        
        // Track this connection
        if (!this.userConnections.has(userId)) {
            this.userConnections.set(userId, new Set());
        }
        this.userConnections.get(userId).add(ws);
        
        // Clean up on disconnect
        ws.on('close', () => {
            this.userConnections.get(userId).delete(ws);
            if (this.userConnections.get(userId).size === 0) {
                this.userConnections.delete(userId);
            }
        });
    }
}

Client-side (simplified):

// Lightning Component - much simpler!
export default class OptimizedNotifications extends LightningElement {
    websocket;
    
    connectedCallback() {
        // Single WebSocket connection per tab
        this.websocket = new WebSocket('wss://your-heroku-app.herokuapp.com');
        this.websocket.onmessage = (event) => {
            const eventData = JSON.parse(event.data);
            this.showNotification(eventData);
        };
    }
    
    disconnectedCallback() {
        if (this.websocket) {
            this.websocket.close();
        }
    }
}

Benefits:

  • Guaranteed 1 Platform Event delivery per event (to server)
  • Perfect deduplication (server controls fan-out)
  • User-specific filtering (server-side logic)
  • Real-time delivery to all user's tabs
  • No client coordination complexity

Trade-offs:

  • ❌ Additional infrastructure (Heroku/external service)
  • ❌ WebSocket connection management
  • ❌ Authentication complexity
  • ❌ Network latency (extra hop)

Approach 2: Polling-Based Notification System

Replace Platform Events entirely with a polling-based approach for async process notifications.

Database Design:

-- Custom Object: User_Notification__c
Id, User__c, Process_Type__c, Message__c, Status__c, 
Created_Date__c, Read_Date__c, Process_Id__c

Server-side (Apex):

// When async process completes
public class AsyncProcessNotifier {
    public static void notifyProcessComplete(String processType, String message, Id userId) {
        User_Notification__c notification = new User_Notification__c(
            User__c = userId,
            Process_Type__c = processType,
            Message__c = message,
            Status__c = 'Unread',
            Process_Id__c = generateProcessId()
        );
        insert notification;
    }
    
    @AuraEnabled
    public static List<User_Notification__c> getUnreadNotifications() {
        return [
            SELECT Id, Process_Type__c, Message__c, Created_Date__c
            FROM User_Notification__c 
            WHERE User__c = :UserInfo.getUserId() 
            AND Status__c = 'Unread'
            ORDER BY Created_Date__c DESC
        ];
    }
    
    @AuraEnabled  
    public static void markNotificationsRead(List<Id> notificationIds) {
        List<User_Notification__c> notifications = [
            SELECT Id FROM User_Notification__c WHERE Id IN :notificationIds
        ];
        
        for (User_Notification__c notif : notifications) {
            notif.Status__c = 'Read';
            notif.Read_Date__c = System.now();
        }
        update notifications;
    }
}

Client-side (polling):

export default class PollingNotifications extends LightningElement {
    @wire(getUnreadNotifications) 
    wiredNotifications({ data, error }) {
        if (data && data.length > 0) {
            this.processNewNotifications(data);
        }
    }
    
    connectedCallback() {
        // Poll every 30 seconds instead of real-time events
        this.pollingInterval = setInterval(() => {
            refreshApex(this.wiredNotifications);
        }, 30000);
    }
    
    processNewNotifications(notifications) {
        notifications.forEach(notif => {
            this.showToast(notif.Process_Type__c, notif.Message__c);
        });
        
        // Mark as read to prevent showing again
        const notifIds = notifications.map(n => n.Id);
        markNotificationsRead({ notificationIds: notifIds });
    }
}

Benefits:

  • Zero Platform Event allocation usage
  • Perfect deduplication (database-driven)
  • Audit trail (all notifications stored)
  • User preference management (notification settings)
  • No coordination complexity
  • Works across devices seamlessly

Trade-offs:

  • ❌ Not real-time (30-60 second delays)
  • ❌ Additional database storage
  • ❌ SOQL query limits (if high volume)
  • ❌ More complex read/unread state management

Approach 3: Hybrid Event + Database Pattern

Combine the best of both approaches: use Platform Events for real-time delivery but database for guaranteed delivery.

Implementation:

public class HybridNotificationSystem {
    public static void sendNotification(String message, Id userId, String priority) {
        // 1. Always store in database (guaranteed delivery)
        User_Notification__c dbNotification = new User_Notification__c(
            User__c = userId,
            Message__c = message,
            Status__c = 'Pending',
            Priority__c = priority
        );
        insert dbNotification;
        
        // 2. Try Platform Event for real-time (best effort)
        try {
            Messaging_Utility_Event__e platformEvent = new Messaging_Utility_Event__e(
                Message_Body__c = message,
                Target_User_Id__c = userId,
                Database_Id__c = dbNotification.Id,
                Message_Variant__c = priority.toLowerCase()
            );
            EventBus.publish(platformEvent);
        } catch (Exception e) {
            // Platform Event failed, but database notification exists
            System.debug('Platform Event failed, falling back to database: ' + e.getMessage());
        }
    }
}

Client-side coordination:

export default class HybridNotifications extends LightningElement {
    receivedEventIds = new Set();
    
    connectedCallback() {
        // Subscribe to Platform Events (when working)
        this.subscribeToPlatformEvents();
        
        // Fallback polling for missed events
        this.startFallbackPolling();
    }
    
    handlePlatformEvent(response) {
        const eventData = response.data.payload;
        const dbId = eventData.Database_Id__c;
        
        // Track that we received this via Platform Event
        this.receivedEventIds.add(dbId);
        
        // Show notification immediately
        this.showNotification(eventData);
        
        // Mark as read in database
        markNotificationRead({ notificationId: dbId });
    }
    
    startFallbackPolling() {
        // Check database every 60 seconds for missed notifications
        setInterval(() => {
            this.checkForMissedNotifications();
        }, 60000);
    }
    
    async checkForMissedNotifications() {
        const unreadNotifications = await getUnreadNotifications();
        
        unreadNotifications.forEach(notif => {
            if (!this.receivedEventIds.has(notif.Id)) {
                // We missed this via Platform Event, show it now
                this.showNotification(notif);
                markNotificationRead({ notificationId: notif.Id });
            }
        });
    }
}

Benefits:

  • Real-time when Platform Events work
  • Guaranteed delivery via database fallback
  • Automatic gap detection and recovery
  • Reduced Platform Event usage (only when needed)
  • Audit trail and analytics

Approach 5: Salesforce Notifications (Bell Icon)

The often-overlooked native solution that might solve your problem without any custom development.

How it works:

// Apex - Send notification when async process completes
public class AsyncProcessNotifier {
    public static void notifyProcessComplete(String title, String body, Id targetUserId, Id recordId) {
        // Create custom notification
        messaging.CustomNotification notification = new messaging.CustomNotification();
        
        // Set notification properties
        notification.setTitle(title);
        notification.setBody(body);
        notification.setNotificationTypeId(getNotificationTypeId()); // Configure in Setup
        notification.setTargetId(recordId); // Optional: link to specific record
        
        // Send to specific user(s)
        Set<String> recipientsIds = new Set<String>{targetUserId};
        notification.send(recipientsIds);
    }
    
    private static String getNotificationTypeId() {
        CustomNotificationType notificationType = [
            SELECT Id FROM CustomNotificationType 
            WHERE DeveloperName = 'Async_Process_Notifications'
            LIMIT 1
        ];
        return notificationType.Id;
    }
}

Setup Required:

  1. Setup → Custom Notifications → New Notification Type

    • Name: "Async Process Notifications"
    • Desktop: ✓ In-App, ✓ Push (optional)
    • Mobile: ✓ In-App, ✓ Push
  2. User Settings → Notifications → Configure preferences

    • Users control when/how they receive notifications
    • Automatic mobile push integration

Client-side (automatic):

// NO CODE NEEDED! 
// Notifications appear automatically in:
// - Bell icon in Salesforce header
// - Mobile push notifications (if enabled)
// - Email digest (if configured)
// - Desktop push (if enabled)

Benefits:

  • Zero Platform Event allocation usage
  • Native Salesforce integration (bell icon)
  • Automatic mobile push notifications
  • User preference management (users control frequency)
  • Cross-device synchronization built-in
  • Read/unread state management automatic
  • Audit trail (notification history)
  • No custom UI development required
  • Record linking (click notification → go to record)
  • Batching and digest options (avoid spam)

Trade-offs:

  • Limited customization (can't control exact appearance)
  • No real-time toasts (appears in bell, not as toast)
  • Requires notification type setup (admin configuration)
  • User adoption (users must check bell icon)
  • Mobile app required for push (not web-based push)

When to use Salesforce Notifications:

  • Async process completion alerts
  • System-generated notifications
  • Cross-device delivery important
  • Want native mobile push
  • Users already use Salesforce mobile app
  • Don't need immediate toast-style alerts

Updated Recommendation Matrix

Use Case Recommended Approach Platform Event Usage Setup Complexity
< 50 users, low volume Salesforce Notifications Zero Low
< 100 users, need toasts Client-side coordination Optimized Medium
100-500 users, cross-device Salesforce Notifications + Database Zero Low
500-1000 users, real-time critical Hybrid Event + Database Reduced Medium
1000+ users, high volume Server-side fan-out Minimal High
Critical processes Email + Platform Events Minimal Low
Team notifications Slack/Teams Integration Zero Medium
Audit/compliance heavy Database polling Zero Medium
Mobile-first users Salesforce Notifications Zero Low
Custom UI requirements Server-side fan-out Minimal High

The "Start Here" Decision Tree

Step 1: Are your notifications for async process completion?

  • Yes → Try Salesforce Notifications first (zero Platform Events)
  • No → Continue to Step 2

Step 2: Do you need immediate toast-style alerts in the UI?

  • Yes → Continue to Step 3
  • No → Use Salesforce Notifications or Email

Step 3: Are you hitting Platform Event delivery limits?

  • No → Client-side coordination may be sufficient
  • Yes → Continue to Step 4

Step 4: Can you add external infrastructure?

  • Yes → Server-side fan-out (best solution)
  • No → Hybrid Event + Database approach

Step 5: Still having issues?

  • → Consider if you actually need real-time notifications
  • → Most async processes work fine with Salesforce Notifications

Scenarios Where You Still Get Multiple Deliveries

1. Failover Windows

10:15:30 - Primary tab crashes unexpectedly
10:15:31 - Event published during failover
10:15:45 - Secondary tab detects primary is dead (15-second window)
10:15:46 - Secondary becomes new primary

Result: Events published during seconds 31-46 aren't delivered to anyone
        Events published after 46 get delivered to new primary
        If old primary "resurrects" briefly, double delivery possible

2. Network Partition Scenarios

User on flaky WiFi:
- Primary tab loses connectivity temporarily
- Secondary tab detects "dead" primary and takes over  
- Primary tab reconnects and doesn't realize it lost coordination
- Both tabs are now "primary" until next heartbeat cycle

Result: 2x delivery allocation until coordination self-heals

3. Browser Resource Management

Browser under memory pressure:
- Suspends background tabs to free resources
- Primary tab gets suspended mid-heartbeat
- Secondary takes over thinking primary died
- Browser unsuspends original primary later
- Now you have competing primaries

This is especially common on mobile browsers and resource-constrained devices

4. Cross-Device Reality

User workflow:
- Desktop: 3 tabs open (1 becomes primary)
- Mobile: Opens Salesforce app during meeting
- Tablet: Checks dashboard over lunch

Result: 3 separate "primary" subscriptions across devices
        Tab coordination only works within same browser instance

5. localStorage Corruption/Clearing

User actions that break coordination:
- Browser extension clears localStorage
- User manually clears browser data  
- Incognito mode doesn't inherit coordination state
- Different sub-domains (my.salesforce.com vs custom.salesforce.com)

Result: All tabs think they're first and become primary

The Dirty Little Secrets

We still see occasional spikes:

Before optimization: Daily spikes of 200k-250k deliveries
After optimization:  Daily spikes of 80k-100k deliveries

That's still 20k-30k "waste" deliveries, but it's manageable

Why 100% elimination is impossible:

  • Physics: Network delays, browser limitations, timing windows
  • User behavior: Unpredictable device usage patterns
  • Salesforce: Platform Event delivery happens server-side before client coordination
  • Scale: Edge cases multiply with more users

The 80/20 Reality Check

This solution gets you 80% of the benefit with 20% of the perfect coordination complexity.

What it reliably prevents:

  • ✅ Normal multi-tab usage multiplication (biggest win)
  • ✅ Meeting rush orphaned subscriptions
  • ✅ Predictable daily allocation spikes
  • ✅ Users getting 4 identical notifications

What it can't prevent:

  • ❌ All edge case scenarios
  • ❌ Cross-device subscription multiplication
  • ❌ Network partition temporary doubles
  • ❌ Browser resource management interference

When "Good Enough" Is Good Enough

Consider this optimization successful if:

  • Daily allocation usage drops 50-70%
  • Meeting-time spikes become manageable
  • You stop hitting delivery allocation limits
  • Users report better notification experience

Don't chase 100% perfection because:

  • Diminishing returns on additional complexity
  • Edge cases affect <5% of usage
  • Perfect coordination would require server-side changes
  • Salesforce doesn't provide the primitives for perfect client coordination

The Pragmatic Client-Side Approach

1. Monitoring and Alerting

// Track coordination health
if (subscriptionCount / userCount > 2.5) {
    console.warn('Coordination degrading - investigate');
}

2. Graceful Degradation

// If coordination fails, fallback gracefully
if (this.coordinationFailed) {
    // Still subscribe, but with exponential backoff
    this.subscribeWithBackoff();
}

3. Business Impact Focus

Success metric: "Users get notifications reliably"
NOT: "Zero duplicate deliveries ever"

The Bottom Line

Platform Events are incredibly powerful, but the delivery model can surprise you with hidden costs. A single user with multiple tabs can consume 4x your expected allocation without you realizing it.

The honest truth: This optimization won't give you perfect coordination, but it will solve 80% of your delivery allocation problems with reasonable engineering effort.

The tab coordination pattern isn't just about saving money (though 50-70% cost reduction is significant). It's about reliability. When you hit allocation limits, Platform Events stop delivering altogether. Your real-time notifications become... not real-time.

What I wish I'd known from the start:

First, understand that Platform Events charge per subscription, not per user. That distinction matters more than you think. Monitor your multiplication factor (active subscriptions vs. user sessions) to spot problems early.

Don't try to build the perfect solution immediately - go for the 80% win with client-side coordination, and don't chase perfection. Browser behavior is wildly inconsistent across different tab operations, so test thoroughly. Plan for the edge cases that will still happen, with good monitoring and graceful degradation.

Most importantly, focus on business impact. Reliable notifications that occasionally duplicate are infinitely better than perfect deduplication that fails when you hit allocation limits.

The next time you see a mysterious 3x spike in Platform Event deliveries, remember: it might not be your code that's broken. It might just be your users opening multiple tabs.


Have you encountered Platform Event delivery allocation issues? Share your experiences in the comments below. And if you implement this pattern, I'd love to hear about your results!

Resources:

This post is based on real optimization work done on a production Salesforce org with 200+ daily active users and 25,000+ daily Platform Events.

Comments (0)

Loading comments...