Installing airgap.js: Synchronous vs Asynchronous

This guide gives you the exact setup for both ways to load airgap.js, with complete, paste-ready markup.

airgap.js regulates network requests, cookies, and DOM insertions like a client-side firewall. It can only regulate what happens after it initializes.

Nothing may track before airgap.js is ready.

There are exactly two correct ways to honor that rule:

  1. Synchronous: load airgap.js as the very first script, so it is already regulating by the time anything else runs. You then add trackers normally.
  2. Asynchronous: load airgap.js off the critical path and gate every tracker, so none start until airgap.js is ready.

Use synchronous loading. It is the simplest setup that is correct by default: airgap regulates everything after it, so you don't have to touch your tracker code. The one <script> in <head> blocks briefly while the bundle loads.

Choose asynchronous only if you have a measured need to keep airgap.js off the critical rendering path (e.g. strict LCP budgets). The tradeoff: you become responsible for gating every tracker, and any that slips through fires unregulated.

Synchronous (recommended)Asynchronous
How airgap loadsBlocking <script>, first in <head>Injected async after an inline stub
Who guarantees "nothing tracks first"airgap (it's first)You gate every tracker
Tracker code changesNone; add trackers normallyEvery tracker moved behind a ready gate
Best forAlmost everyoneStrict performance budgets, SPAs

How it works: because airgap.js is the first script on the page, it is already intercepting requests before any tracker runs. You add analytics and marketing scripts exactly as their vendors document them, with no gating or changes on your side, and airgap regulates them for you.

Copy it from Developer Settings → Installation. Your bundle URL looks like https://transcend-cdn.com/cm/<your-bundle-id>/airgap.js.

It must come before any script, pixel, or resource hint that could make a tracking request.

Place every tracker below the airgap tag; they load normally.

Everything below the airgap tag is regulated automatically. The airgap tag is first in <head>, and the trackers (Amplitude and a marketing pixel) come after it, unmodified.

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />

    <!-- 1) Load airgap.js first, before all other scripts/trackers.
         data-cfasync="false" stops Cloudflare Rocket Loader (and similar
         optimizers) from deferring or reordering it. -->
    <script
      data-cfasync="false"
      src="https://transcend-cdn.com/cm/<your-bundle-id>/airgap.js"
    ></script>

    <!-- Everything below is regulated by airgap.js -->

    <!-- 2) Amplitude, load its library, then initialize. -->
    <script src="https://cdn.amplitude.com/libs/analytics-browser-2-min.js"></script>
    <script>
      window.amplitude.init("AMPLITUDE_API_KEY", { autocapture: true });
    </script>

    <!-- 3) Any other (non-Google) marketing pixel / tag. -->
    <script src="https://example-pixel.com/pixel.js"></script>

    <!-- Google tags (gtag.js / GTM)? See the Google Consent Mode section below. -->
  </head>
  <body>
    <!-- your page -->
  </body>
</html>
  • Keep data-cfasync="false". Cloudflare Rocket Loader and similar tools will otherwise defer/reorder the tag and break the "first script" guarantee.
  • Put airgap above resource hints that fetch, such as <link rel="preload">/preconnect for tracker hosts.
  • Using a tag manager (e.g. GTM)? Put the airgap.js tag above the container snippet so the non-Google tags it injects are regulated at the network level. Don't rely on the tag manager itself to enforce consent; it's leaky. For Google tags, use Consent Mode (see the Google Consent Mode section below).
  • Server-rendered React, Next.js, or Hydrogen? Render this exact tag as the first element of <head> on both server and client. If the tag only exists in server HTML, hydration can remove it and break initialization. If that's hard, use async loading (Part 2) or see the React Snippets guide.

How it works: you load airgap.js off the critical path. Until it's ready, nothing is regulated, so you must hold back every tracker and start each one only once airgap signals ready.

Recommended pattern: a tiny inline pre-init stub (synchronous, so the ready queue exists immediately) that exposes a memoized getAirgap() promise. Every tracker initializes inside await getAirgap(), so airgap loads once and trackers start only when it's regulating.

Same as above, from Developer Settings → Installation.

This inline block is synchronous and tiny. It defines the ready queue and injects airgap.js asynchronously. Put it first so the queue exists before anything else runs.

Load each tracker's library and start it inside the ready gate, using a small loadScript helper, so even the library fetch waits until airgap.js is regulating. Nothing that touches the network may run outside the gate.

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />

    <!-- 1) First: pre-init stub + getAirgap() helper (inline, synchronous, tiny).
         This defines the ready queue and injects airgap.js asynchronously. -->
    <script>
      // Get your bundle ID at
      // https://app.transcend.io/consent-manager/developer-settings/installation
      var BUNDLE_ID = '<your-bundle-id>';

      var airgapAPI;
      window.getAirgap = function () {
        return (airgapAPI =
          airgapAPI ||
          new Promise(function (resolve, reject) {
            // Stub the ready queue before airgap.js exists.
            if (!(self.airgap && self.airgap.ready)) {
              self.airgap = Object.assign(
                {
                  readyQueue: [],
                  ready: function (cb) { this.readyQueue.push(cb); },
                },
                self.airgap
              );
            }

            // Resolve once airgap.js has loaded and is ready.
            self.airgap.ready(function (airgap) { resolve(airgap); });

            // Inject airgap.js asynchronously.
            var s = document.createElement('script');
            s.addEventListener('error', reject);
            s.async = s.defer = true;
            // Optional load options, e.g.:
            // s.dataset.lazyLoadUi = 'on';
            s.src = 'https://transcend-cdn.com/cm/' + BUNDLE_ID + '/airgap.js';
            document.documentElement.appendChild(s);
          }));
      };
    </script>

    <!-- 2) Gate: after airgap.js is ready, load each tracker's script,
         then initialize it. Even the library fetch happens after airgap is
         regulating, so nothing touches the network too early. -->
    <script>
      // Inject a <script> element and resolve once it has loaded.
      function loadScript(src) {
        return new Promise((resolve, reject) => {
          const script = document.createElement('script');
          script.async = true;
          script.src = src;
          script.onload = () => resolve();
          script.onerror = reject;
          document.head.appendChild(script);
        });
      }

      (async () => {
        // 1. Wait until airgap.js is loaded and regulating.
        await getAirgap();

        // 2. Amplitude: load its library, then initialize.
        await loadScript("https://cdn.amplitude.com/libs/analytics-browser-2-min.js");
        window.amplitude.init("AMPLITUDE_API_KEY", { autocapture: true });

        // 3. Any other (non-Google) marketing pixel / tag.
        await loadScript("https://example-pixel.com/pixel.js");

        // Google tags (gtag.js / GTM)? See the Google Consent Mode section below.
      })();
    </script>
  </head>
  <body>
    <!-- your page -->
  </body>
</html>

Beware: you are now importing airgap.js asynchronously. Since it can only regulate traffic after it loads, you are responsible for ensuring no trackers load before airgap.js. The caveat applies only to the window before init; once airgap is ready, quarantine and replay handle later consent changes on the same page.

  • Load tracker libraries inside the gate, too. Don't add ungated tracker <script> tags to <head>; use the loadScript helper so even the library fetch happens after airgap.js is regulating.
  • Extend, never overwrite self.airgap. The stub above uses Object.assign({…}, self.airgap) for exactly this reason: clobbering it destroys any queue another script already registered.
  • React, Next.js, or Hydrogen: load airgap once at app startup (a provider), gate trackers with a hook, and remember the async React pattern does not load airgap first. Use the React Snippets for airgap guide.

This is the same whether you load airgap synchronously or asynchronously. Google tags (gtag.js / GTM) are a special case: don't gate them behind airgap. Google Consent Mode is a signal integration: airgap passes consent state to Google and Google's own tags self-regulate (in Advanced mode they load early and send cookieless pings before consent), so gating would suppress that.

Instead, set safe denied consent defaults synchronously, before airgap.js loads. Then load airgap (the sync tag from Part 1, or the async stub from Part 2) and let your Google tags run normally; airgap pushes the regime default and consent updates as they resolve. Your non-Google trackers still follow the mode you chose: after airgap in sync, inside the gate in async.

<!-- FIRST in <head>: denied Consent Mode defaults (synchronous, before airgap.js) -->
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){ dataLayer.push(arguments); }
  gtag('set', 'developer_id.dODQ2Mj', true); // Transcend Consent Mode developer ID
  gtag('consent', 'default', {
    analytics_storage: 'denied',
    ad_storage: 'denied',
    ad_user_data: 'denied',
    ad_personalization: 'denied',
    functionality_storage: 'denied',
    personalization_storage: 'denied',
    security_storage: 'denied',
  });
</script>
<!-- then load airgap.js, and your Google tags load normally -->

The snippet above is for sites that load Google tags directly with gtag.js. With Google Tag Manager, keep your standard container snippet; the one thing to change is placement. Google tells you to paste the GTM container "as high in <head> as possible," but with Transcend the denied defaults and airgap.js go above the GTM container, so airgap regulates everything GTM injects. The GTM <body> <noscript> block is unchanged.

<!doctype html>
<html>
  <head>
    <!-- 1) Denied Consent Mode defaults (synchronous, first) -->
    <script>
      window.dataLayer = window.dataLayer || [];
      function gtag(){ dataLayer.push(arguments); }
      gtag('set', 'developer_id.dODQ2Mj', true); // Transcend Consent Mode developer ID
      gtag('consent', 'default', {
        analytics_storage: 'denied',
        ad_storage: 'denied',
        ad_user_data: 'denied',
        ad_personalization: 'denied',
        functionality_storage: 'denied',
        personalization_storage: 'denied',
        security_storage: 'denied',
      });
    </script>

    <!-- 2) airgap.js, above the GTM container so it regulates everything GTM
         injects. (Loading airgap async? Use the Part 2 stub here instead.) -->
    <script
      data-cfasync="false"
      src="https://transcend-cdn.com/cm/<your-bundle-id>/airgap.js"
    ></script>

    <!-- 3) Your standard GTM container snippet, unchanged -->
    <!-- Google Tag Manager -->
    <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
    new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
    j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
    'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
    })(window,document,'script','dataLayer','GTM-XXXXXXX');</script>
    <!-- End Google Tag Manager -->
  </head>
  <body>
    <!-- Google Tag Manager (noscript), unchanged -->
    <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
      height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
    <!-- your page -->
  </body>
</html>

Prefer not to touch page code? Set the defaults inside GTM on the built-in Consent Initialization – All Pages trigger, or load airgap from within GTM using Transcend's GTM template. Basic vs Advanced mode, the purpose mapping, and the full GTM template are covered in the Google Consent Mode guide.

  1. Network tab: hard-reload and confirm no tracker request fires before airgap.js. In sync mode, airgap.js is the first request; in async mode, no tracker should hit the network until after it.
  2. airgap.export(): run it in the console before and after granting consent on the same page. Blocked requests should appear while unconsented and drain after consent. (If they appear but never drain, you have an initialization/hydration problem.)
  3. Google Tag Assistant (if using GCM): confirm you do not see "A tag read consent state before a default was set."

See Consent Management Debugging and Testing for more.