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.
Server-side evaluation
Section titled “Server-side evaluation”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.
Caching the feature payload
Section titled “Caching the feature payload”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.
Why per-request scoping matters
Section titled “Why per-request scoping matters”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
}Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
| Server-side SDK | Safer, full control over backend logic | You manage caching/refresh yourself so you don’t query on every request |
| Call the API directly on every request | Easier to reason about | Higher latency and more load on GrowthBook’s backend service |
Common mistakes
Section titled “Common mistakes”- Re-fetching the feature payload on every request instead of fetching it once and reusing it — creating a new lightweight instance per request is fine and recommended; it’s the network fetch of the payload that’s expensive
- Not caching the feature payload, so every request makes a network call to the CDN
- Forgetting to refresh the payload when a flag changes in the UI
💡 Real-world example
Larger backend services typically fetch the feature payload once per process, refresh it in the background and keep it in memory, then create a cheap new instance per request that reuses the cached payload — giving both per-request targeting and near-zero latency since there’s no re-fetch.