SharedArrayBuffer Without the Headaches: A Practical COOP/COEP Setup Guide

The Problem Nobody Warns You About

You’re building something that needs serious parallel computation in the browser — a WebAssembly module, a video encoder, an FFT library, a physics engine. You reach for SharedArrayBuffer because it’s the only way to share memory between workers without copying megabytes back and forth. You test it locally. Works great.

You deploy. It’s dead. Zero. SharedArrayBuffer is not defined.

Welcome to the Spectre aftermath.

In January 2018, Google disclosed the Spectre CPU vulnerability. The attack uses high-resolution timers to leak data from memory across security boundaries. SharedArrayBuffer, combined with a Worker thread spinning in a tight loop, is essentially a precise high-resolution timer. Browsers yanked it within days. Chrome re-enabled it in 2020, but only under a strict set of conditions that most tutorials don’t fully explain.

This article walks through exactly what those conditions are, why they exist, how to configure them correctly, and — critically — what will break when you flip these headers on a real app.


What "Cross-Origin Isolated" Actually Means

The browser will re-enable SharedArrayBuffer (and performance.measureUserAgentSpecificMemory(), and high-precision performance.now()) only when the page is in a cross-origin isolated state. You can check this from JavaScript:

console.log(self.crossOriginIsolated); // true or false

To reach that state, your page must be served with both of these HTTP response headers:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

That’s the minimum viable config. Let’s understand what each one actually does before you blindly copy it into your nginx config.


Cross-Origin-Opener-Policy (COOP)

COOP controls whether your page shares a browsing context group with other pages. A browsing context group is basically a set of tabs/windows that can reach each other via window.opener, window.open(), and postMessage. If two pages share a context group, they share the same OS process in Chromium-based browsers — and that’s the attack surface Spectre exploits.

Three valid values:

  • unsafe-none — the default. Your page joins whatever context group the opener is in. No isolation at all.
  • same-origin-allow-popups — your page gets its own context group, but it can still open popups to cross-origin pages (those popups just won’t be able to reference back to you).
  • same-origin — strict isolation. Your page gets its own context group. Cross-origin pages that try to open your page will lose the window.opener reference.

For crossOriginIsolated to be true, you need same-origin.

The practical consequence: OAuth flows that open a popup will break unless the OAuth provider also sends COOP: same-origin. They almost certainly don’t. More on this in the Gotchas section.


Cross-Origin-Embedder-Policy (COEP)

COEP controls what cross-origin resources your page is allowed to load. The idea: if your page is process-isolated, you don’t want arbitrary cross-origin content leaking into that process and potentially being probed via timing attacks.

Three values:

  • unsafe-none — the default, no restrictions.
  • require-corp — every cross-origin resource must either be same-origin or explicitly opt-in via a Cross-Origin-Resource-Policy (CORP) header. If a resource doesn’t have CORP, it fails to load.
  • credentialless — cross-origin resources can load, but without cookies or credentials. A middle ground introduced in Chrome 96 and now broadly supported.

require-corp is the stricter option and the one required by the spec for crossOriginIsolated. credentialless also satisfies the requirement in browsers that support it, and is often easier to deploy on existing apps.


Step 1: Add the Headers to Your Server

Nginx

server {
    listen 443 ssl;
    server_name your-app.example.com;

    # Enable cross-origin isolation
    add_header Cross-Origin-Opener-Policy "same-origin" always;
    add_header Cross-Origin-Embedder-Policy "require-corp" always;

    # ...rest of your config
}

The always flag is important — nginx only sends add_header directives on 200/304 responses by default. You want these headers on error pages too, or a redirect to a 403 could strip the headers and confuse the browser.

Apache

<VirtualHost *:443>
    ServerName your-app.example.com

    Header always set Cross-Origin-Opener-Policy "same-origin"
    Header always set Cross-Origin-Embedder-Policy "require-corp"
</VirtualHost>

Make sure mod_headers is enabled: a2enmod headers.

Caddy

your-app.example.com {
    header Cross-Origin-Opener-Policy same-origin
    header Cross-Origin-Embedder-Policy require-corp
}

Express / Node.js

app.use((req, res, next) => {
  res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
  res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
  next();
});

Step 2: Fix Your Cross-Origin Resources

This is where the real work is. With COEP: require-corp, every cross-origin resource — images, fonts, scripts, stylesheets, API responses — must send one of these response headers:

Cross-Origin-Resource-Policy: same-origin
Cross-Origin-Resource-Policy: same-site
Cross-Origin-Resource-Policy: cross-origin

cross-origin means "any page can embed me," which is what most CDN-hosted public resources should be returning anyway. The problem is that most of them don’t.

For resources you control (your own CDN, your own API):

# On the resource server (CDN, API, asset server)
add_header Cross-Origin-Resource-Policy "cross-origin" always;

For resources you don’t control — Google Fonts, third-party analytics scripts, embedded widgets — you have a few options:

  1. Self-host them. Best option for fonts and libraries. Degrades gracefully, removes a third-party dependency, and improves cache predictability.
  2. Proxy them through your own domain. Your server fetches and re-serves them with the right CORP header. More ops overhead but workable.
  3. Switch to COEP: credentialless. Resources load without credentials, CORP check is skipped for no-CORS requests. Easier migration but slightly weaker isolation semantics.
  4. Remove the dependency. If you can’t get CORP headers on a third-party resource and you can’t self-host or proxy it, drop it.

Step 3: Handle Iframes

If you embed cross-origin iframes, they need to opt in explicitly. The iframe’s origin must send:

Cross-Origin-Embedder-Policy: require-corp

…and your page must include the credentialless attribute on the <iframe> element if you’re using COEP: credentialless, or the embedded page must itself be cross-origin isolated if you’re using require-corp.

For iframes you control across subdomains, this is manageable. For third-party embeds (YouTube, Stripe, Intercom), you’re at their mercy — and most of them don’t support cross-origin isolation yet.

The workaround: load those iframes in a separate, non-isolated page and communicate via postMessage. Yes, it’s annoying. That’s the cost of the isolation model.


Step 4: Test Before Deploying

Do not flip these headers straight to enforcement on production. Use the report-only variants first:

Cross-Origin-Opener-Policy-Report-Only: same-origin; report-to="coop-endpoint"
Cross-Origin-Embedder-Policy-Report-Only: require-corp; report-to="coep-endpoint"

You’ll need a Reporting API endpoint to collect violations. A simple setup:

Reporting-Endpoints: coop-endpoint="https://your-app.example.com/csp-reports"

The browser will POST JSON violation reports to that URL. Log them, look at what breaks, fix it before enforcing.

Chrome DevTools also shows COEP violations in the Console tab — look for messages about blocked cross-origin resources. That’s faster for local development than parsing violation reports.


Gotchas

OAuth Popups

This is the most common production breakage. Your login flow opens accounts.google.com in a popup. After auth, Google redirects back to your origin. With COOP: same-origin, the popup and the opener are in separate context groups — the popup can’t call window.opener.postMessage() or set window.opener.location.

The fix depends on your OAuth implementation. If you’re using a redirect flow instead of a popup, COOP: same-origin doesn’t affect it. If you need the popup flow, some providers support a postMessage-based callback that works within COOP constraints. Check your provider’s docs.

For providers that don’t support this: use redirect-based OAuth or accept that your login flow needs to reload the main page after auth rather than using window.opener.

window.open() for Non-Auth Cases

Any code that opens a cross-origin popup and then communicates back via window.opener will break. Audit your codebase for window.open() calls and how the opened window communicates back.

Fonts from Google Fonts

Google Fonts CDN does not send Cross-Origin-Resource-Policy: cross-origin. Your fonts will fail to load with COEP: require-corp. Self-host your fonts. Seriously, you should be doing this anyway — it removes a DNS lookup, prevents a potential privacy leak (Google can see who’s loading your fonts), and gives you full control over caching.

Download fonts from Google Fonts, host them on your server, update your CSS @font-face declarations. Done.

Service Workers

Service workers intercept requests. If your service worker fetches cross-origin resources without CORP headers, those fetches will be blocked under COEP: require-corp. Update your service worker’s fetch handler to either only cache same-origin resources or ensure cross-origin resources have CORP headers.

Lazy-Loaded Third-Party Scripts

Tag managers, analytics scripts, ad networks — most of these inject cross-origin content at runtime. With COEP: require-corp, those resources get blocked silently (or not so silently — check the console). You’ll need to evaluate whether those third-party scripts are compatible with your isolation requirements. For privacy-focused apps, this is actually a feature. For ad-supported apps, this is a real constraint.

Safari

Safari added support for SharedArrayBuffer with crossOriginIsolated in Safari 15.2. If you’re targeting older Safari versions, you can’t use SharedArrayBuffer regardless of your headers. Check your analytics before investing in this.


Production-Ready Nginx Config

Here’s a complete example that includes the security headers, report-only mode during rollout, and a reporting endpoint:

server {
    listen 443 ssl http2;
    server_name your-app.example.com;

    ssl_certificate /etc/ssl/certs/your-cert.pem;
    ssl_certificate_key /etc/ssl/private/your-key.pem;

    # Phase 1: report-only (use during testing)
    # add_header Cross-Origin-Opener-Policy-Report-Only "same-origin; report-to=\"coop-reports\"" always;
    # add_header Cross-Origin-Embedder-Policy-Report-Only "require-corp; report-to=\"coep-reports\"" always;

    # Phase 2: enforcement (switch to this after fixing all violations)
    add_header Cross-Origin-Opener-Policy "same-origin" always;
    add_header Cross-Origin-Embedder-Policy "require-corp" always;

    # Reporting API endpoint declaration
    add_header Reporting-Endpoints "coop-reports=\"https://your-app.example.com/_reports/coop\", coep-reports=\"https://your-app.example.com/_reports/coep\"" always;

    root /var/www/your-app;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    # Your app handles POST /_reports/*
    location /_reports/ {
        proxy_pass http://127.0.0.1:3000;
    }
}

And for your asset server or CDN origin:

server {
    listen 443 ssl http2;
    server_name assets.your-app.example.com;

    location ~* \.(js|css|woff2|woff|ttf|png|jpg|webp|svg)$ {
        # Allow any origin to embed these assets
        add_header Cross-Origin-Resource-Policy "cross-origin" always;

        # Standard cache headers
        add_header Cache-Control "public, max-age=31536000, immutable" always;

        root /var/www/assets;
        try_files $uri =404;
    }
}

Verifying It Works

After deploying, open DevTools and run:

console.log(self.crossOriginIsolated); // should be true

Then verify SharedArrayBuffer is actually accessible:

const sab = new SharedArrayBuffer(1024);
console.log(sab.byteLength); // 1024

You can also check the response headers in the Network tab. Click on your main document request and verify both COOP and COEP headers are present.

If crossOriginIsolated is still false with both headers present, check:

  1. Are you on HTTPS? crossOriginIsolated requires a secure context (localhost counts as secure for development).
  2. Is the page inside an iframe? The parent frame’s headers also need to be correct.
  3. Are there any COEP violations in the console blocking the transition?

When credentialless Is the Right Call

COEP: require-corp is maximally correct but maximally painful to deploy on an existing app with many third-party dependencies. COEP: credentialless is a pragmatic middle ground:

Cross-Origin-Embedder-Policy: credentialless

Under credentialless, cross-origin resources load without credentials (no cookies, no auth headers). For public resources — images, fonts, public scripts — this is usually fine. For anything that requires authentication, it will silently lose credentials and likely return an empty or error response.

This mode is good for: existing apps with lots of third-party public assets that you can’t easily patch. It’s not good for: apps that load cross-origin authenticated resources (user avatars from a different domain, authenticated API responses embedded in the page).

Both require-corp and credentialless satisfy the crossOriginIsolated requirement in supporting browsers.


The Broader Picture

COOP and COEP are part of a larger web platform initiative to contain the fallout from speculative execution attacks. crossOriginIsolated is currently gating SharedArrayBuffer, high-precision timers, and memory measurement APIs. The same isolation context will likely gate future APIs that need process-level separation.

If you’re building anything computationally serious in the browser — WebAssembly, audio processing, computer vision, simulation — you will hit this requirement. It’s not going away, it’s only going to gate more capabilities over time. Better to understand it properly now than to debug it at 2am when your WebAssembly module stops working after a Chrome update.

The headers themselves are cheap. The audit of your third-party dependencies is where the actual work is. Start there.

👁 Views: 225,562 · Unique visitors: 59,958