Skip to content

Node.js SDK

The same @growthbook/growthbook package that runs in the browser also works in Node.js, so there is no separate server SDK to install. The key difference is the lifecycle model: on the server, flag evaluation happens inside request handlers rather than once at app startup.

In a browser app you typically create one GrowthBook instance for the lifetime of the page. On the server, each HTTP request may represent a completely different user, so you need per-request scoping — create (or clone) a fresh instance for every request and set user-specific attributes on it.

Fetching feature definitions from GrowthBook on every request would add latency and burn bandwidth. Instead, fetch the payload once at startup, store it in memory, then pass it to each per-request instance via gb.init({ payload: cachedPayload }). This skips the network call entirely while keeping flag evaluation accurate.

You can retrieve the payload from the loader instance with gb.getPayload() and later hand it to a fresh instance with gb.setPayload(payload) or by passing it directly to init.

If you shared a single GrowthBook instance across requests and called gb.setAttributes(...) in each handler, concurrent requests would overwrite each other’s user context — leading to incorrect flag evaluations and hard-to-reproduce bugs. A fresh instance per request eliminates this race condition entirely.

import { GrowthBook, setPolyfills } from '@growthbook/growthbook';

// --- App startup: fetch and cache the payload once ---
let cachedPayload = null;

const loader = new GrowthBook({
  apiHost: 'https://cdn.growthbook.io',
  clientKey: 'sdk-abc123',
});
await loader.init({ timeout: 3000 });
cachedPayload = loader.getPayload();

// --- Per-request handler ---
async function handleRequest(req, res) {
  // Fresh instance per request — no shared mutable state
  const gb = new GrowthBook({
    apiHost: 'https://cdn.growthbook.io',
    clientKey: 'sdk-abc123',
  });

  // Reuse the cached payload — no extra network call
  await gb.init({ payload: cachedPayload });

  // Scope attributes to this request's user
  gb.setAttributes({
    id: req.user.id,
    country: req.headers['cf-ipcountry'] ?? 'US',
  });

  const useBetaUI = gb.isOn('beta-ui');
  res.json({ useBetaUI });

  gb.destroy(); // free listeners
}
Why should you create a fresh GrowthBook instance per request on the server?
What does `gb.getPayload()` return?
How do you reuse a cached payload without a new network call?
What should you call after you are done with a server-side GrowthBook instance?