Skip to main content


Optimizely Knowledge Base

Control the timing of Optimizely Web Event dispatch with holdEvents and sendEvents

  • Learn how to use holdEvents and sendEvents JavaScript APIs to address analytics discrepancies and improve performance

Your snippet must be configured to use the /events endpoint in order to use the holdEvents and sendEvents API methods. Follow these instructions to identify the logging endpoint used by your snippet and confirm that it is using the /events endpoint before proceeding.

Optimizely snippets which are configured to use the /events endpoint log batches of events (as opposed to generating one network request per event). These batches are sent once per second for the first ten seconds after snippet activation, and then immediately thereafter. holdEvents and sendEvents work together to control the timing of event logging.  You can use them to better control when the snippet begins sending batches of events. You might want to do this if:

  • You want to improve page load performance by waiting to send events until after the page has finished loading, or

  • You want to more closely align Optimizely’s logging with your analytics platform to address data discrepancies.

Implement the holdEvents and sendEvents APIs

  1. Call holdEvents before the snippet activates (e.g. in Project JavaScript or above the Optimizely snippet tag). This will prevent the snippet from logging any events until sendEvents is called (although any events triggered will be queued in localStorage):

window.optimizely = window.optimizely || [];
window.optimizely.push({type: "holdEvents"});

  1. Call sendEvents when you want to “release” the events. We recommend adding a listener/timeout to call sendEvents from the same place where holdEvents is called, so you make sure both of them are called appropriately. If you call holdEvents but NOT sendEvents, Optimizely will not track any data.

window.optimizely = window.optimizely || [];
window.optimizely.push({type: "sendEvents"});

After calling sendEvents, the snippet will be permitted to send batches of queued events at the next polling interval, and thereafter. Subsequent batches will be sent once per second until 10 seconds have elapsed since the snippet activated. After that point, individual events will be sent as soon as they are triggered. Any events which have not been sent when a visitor navigates will be held in localStorage and sent on the next page where the snippet is implemented, even if the next pageview occurs hours or days later (i.e. at the start of a new session).

Use cases

The two primary use cases for implementing sendEvents and holdEvents each require a distinct implementation strategy.

Improve page load performance

One way to improve page performance is to reduce the number of requests the browser makes during the critical first few seconds after a page starts loading. This is when users are most likely to notice that a page is loading slowly. 

You can ensure that Optimizely doesn’t track any events during this period by calling holdEvents before the snippet activates - as described above - and then binding a call to sendEvents to an event fired by the browser when it finishes loading some part of the page.  

For example, you might want to prevent Optimizely from generating any event requests until after window.load. Your implementation might look something like this:

window.optimizely = window.optimizely || [];
window.optimizely.push({type: "holdEvents"});
window.addEventListener("load", function() {
    window.optimizely.push({type: "sendEvents"});

It’s up to you to pick the most appropriate event to bind the sendEvents call to.  The load event happens after all assets have been downloaded, which is late in the page load process. A more aggressive implementation might use the DOMContentLoaded lifecycle event. With sendEvents, you have the flexibility to align the snippet’s logging behavior to whatever best suits your website’s needs.

Mitigate analytics discrepancies

As discussed in this article on troubleshooting analytics discrepancies, there are several reasons why the numbers you see in Optimizely may not match what you see in your analytics platform. One common cause of discrepancies is timing.  

Typically, Optimizely’s snippet will be implemented high in the head of your pages.  This ensures that Optimizely can initialize quickly and run variation code to manipulate content without causing page flashing. By contrast, analytics tracking code is frequently implemented to fire later during page load, usually in the body of the document. Analytics libraries aren’t responsible for manipulating content and therefore flashing isn’t a concern.

This mismatch in timing means that Optimizely will typically start logging events before your analytics platform does--sometimes several seconds earlier. This leads to Optimizely counting more conversions and unique visitors than your analytics platform, as visitors who exit a page very quickly may be tracked by Optimizely but not your analytics platform.

You can address this timing mismatch by calling holdEvents before the snippet activates, and then calling sendEvents from within a callback function provided by your analytics library.

For example, you might want to prevent Optimizely from generating any event requests until after a Google Analytics tracker object is ready for interaction. You could take advantage of readyCallback to do this. Your implementation might look something like this:

// Implemented above the Optimizely snippet code
    window.optimizely = window.optimizely || [];
    window.optimizely.push({type: "holdEvents"});

// Implemented later on the page
    ga(‘create’, ‘UA-XXXXX-Y’, ‘auto’);
    ga('set', 'anonymizeip', true);
    ga(function(tracker) {
        window.optimizely.push({type: "sendEvents"});

Note that this implementation instructs Optimizely to send events once the GA tracker object is ready for interaction, but this doesn’t mean that a GA event has been fired.  If you want to align Optimizely to a GA event that’s implemented on every page (a pageview event, for example), you could use hitCallback.

Finally, your implementation will vary depending on the analytics platform you are using. Many platforms provide similar callback functionality:


  • mixpanel.init(): fired when initializing the Mixpanel library, utilize the loaded callback in the config object

  • mixpanel.track(): fired when tracking an event with Mixpanel, supports an optional callback function


Impact on results

When implementing holdEvents and sendEvents, the Optimizely snippet may experience a delay before it begins to send events. This will affect Optimizely results in two ways:

  • Optimizely will record fewer unique visitors. Visitors who view one page, exit prior to the first batch of events being sent, and never return to the site will not be tracked.

  • Optimizely will record fewer conversions. Conversions triggered by visitors who immediately exit prior to the batch being sent and never return to the site will not be tracked.

The magnitude of these effects will increase with the amount of time the snippet must wait before it begins to send events. This is desirable if your goal is to align Optimizely’s results with your analytics platform. Note that visitors who convert and then navigate to a new page where the snippet is implemented (as opposed to exiting the site) will be tracked when their events held in localStorage are picked up and sent. This is true even when the next pageview occurs hours or days later (i.e. at the start of a new session).