Adding a feature flag to a one-off script is easy: read the flag, branch, exit. A Node.js backend is a different shape. It boots once, serves thousands of requests across a process that stays up for days, and usually runs as several identical replicas behind a load balancer. That long life is what makes flags in Node a server problem rather than a snippet problem. You have to decide what happens to requests that arrive before flags have loaded, make sure one process holds exactly one client, keep each flag check off the network so it does not slow a hot endpoint, and flush analytics before the process exits.
This guide adds feature flags to a Node.js service with the Node server SDK, which evaluates flags locally in memory so a check costs about as much as a Map.get. The examples use Express, but the pattern is identical for Fastify, Koa, NestJS, or a background worker. For a copy-paste first flag, the Node.js quickstart is the five-minute version. This post is about the operational decisions a real server has to make around it.
Key Takeaways
- A Node server evaluates flags locally: the SDK holds the ruleset in memory and
boolVariation()is a synchronous read, so a flag check adds no network round trip to a request.- Create one client per process.
FeatureflipClient.get()refcounts by SDK key, so importing a shared module from every route is safe and never opens a second streaming connection.- Gate startup on the first flag load. Await initialization before
app.listen()and expose it on a readiness probe, or the first requests after boot silently serve default values.- One dashboard flip reaches every replica. Each instance streams config independently and buckets the same
user_idthe same way, so a percentage rollout stays consistent across the fleet with no sticky sessions and no redeploy.- Close the client on
SIGTERM.close()flushes buffered analytics events and shuts the stream down so nothing is lost when the process exits.
1. Why a long-lived server changes the problem
A flag has three common homes, and the middle one is where most of the work lives.
In a one-off script, flags barely register: load the config, read a value, do the work, exit. In a browser tab, the constraint is the render, which is why a React app cares about re-renders and a Next.js app cares about the hydration flash. A persistent Node.js server sits between those two. It is alive long enough that fetching the ruleset on every request would be wasteful, and busy enough that any per-check latency multiplies across traffic.
That is the case local evaluation is built for. The SDK fetches your full ruleset once at startup, keeps it current over a streaming connection, and answers every boolVariation() from memory. The long-lived process pays the setup cost a single time and then gets effectively free flag checks for the rest of its life. The flip side is that a long-lived process has a lifecycle you have to respect: a boot window where flags are not loaded yet, a shared client that must not be duplicated, and a shutdown where buffered events still need to leave the process. The rest of this guide walks those in order.
2. Install the SDK and create one client for the whole process
Install the server package:
npm install @featureflip/nodeThen create the client once, in its own module, and export it:
import { FeatureflipClient } from '@featureflip/node';
export const flags = FeatureflipClient.get({ sdkKey: process.env.FEATUREFLIP_SDK_KEY!,});FeatureflipClient.get() returns synchronously and kicks off the configuration fetch in the background. Every route file, service, and DI container can import this one flags module. The factory dedupes by SDK key and refcounts the shared core, so even if a library you depend on also calls get() with the same key, the process still holds a single SSE connection, a single event-flush loop, and one in-memory store.
The thing to avoid is constructing a client per request. There is no public constructor for exactly this reason, but the principle is broader than the API: a Featureflip client owns a long-lived streaming connection and background timers, and one per request would open thousands of them. One process, one client. The Lifetime section of the SDK reference has the full refcounting contract if you run a multi-tenant process that serves more than one environment key.
3. Don’t answer requests before flags have loaded
Because get() returns immediately and loads flags in the background, there is a window at boot where the client is alive but has no configuration. A boolVariation() call in that window returns its default value. If your server starts listening during that window, the first users to hit it can land on the “off” path for a feature that is actually on for them, with no error to tell you it happened.
Close the window in two places. First, await initialization before you bind the port:
import express from 'express';import { flags } from './lib/flags';
const app = express();// ... routes ...
async function start() { await flags.waitForInitialization(); app.listen(3000, () => console.log('flags loaded, accepting traffic'));}
start();Second, expose readiness so an orchestrator never routes traffic to a pod that is still booting:
app.get('/readyz', (_req, res) => { res.status(flags.isInitialized ? 200 : 503).send();});waitForInitialization() resolves once the first configuration fetch completes and rejects if it has not finished within initTimeout (10 seconds by default), so a failed fetch surfaces as a failed boot rather than a silently wrong server. On Kubernetes, pointing a readiness probe at /readyz keeps the pod out of the load balancer rotation until flags are in memory, so a rolling deploy never sends a request to an instance that would serve defaults.
4. Keep every flag check off the network
Once flags are loaded, evaluation is a synchronous in-memory read. A variation call takes no await, makes no request, and cannot time out:
app.get('/dashboard', (req, res) => { const newNav = flags.boolVariation( 'new-dashboard-nav', { user_id: req.user.id, plan: req.user.plan }, false, );
res.render(newNav ? 'dashboard-v2' : 'dashboard-v1');});The third argument is the default, returned if the flag is missing or evaluation fails, so a flag check has no failure path of its own to handle. A targeting rule on plan or a percentage rollout resolves locally exactly as it would anywhere else, because the SDK runs the same evaluation logic against the context you pass.
The practical payoff is that you can scatter flag checks through middleware, route handlers, and deep service functions without thinking about request waterfalls. The alternative, calling a flag API over HTTP on each check, puts a network round trip on the hot path and a failure mode in every branch.
5. Flip a flag across every replica at once
Production Node services rarely run as one process. They run as several replicas behind a load balancer, and a request for a given user can land on any of them. Local evaluation handles this without a shared cache to invalidate.
Each instance holds its own in-memory copy of the ruleset and keeps its own streaming connection open. When you flip a flag in the dashboard, the change pushes over that stream to every instance within milliseconds. There is nothing to redeploy and no cache TTL to wait out. Consistency across instances comes from deterministic bucketing: the same user_id hashes to the same position in a percentage rollout on every replica, so a 10% rollout is the same 10% of users no matter which pod answers a given request. You do not need sticky sessions to keep a user on one side of a rollout.
This is the property that makes flags useful as an operational control on a backend, not just a release toggle. Flip a kill switch during an incident and every instance honors it within milliseconds. If a streaming connection drops, the SDK keeps serving the last known configuration while it reconnects, and falls back to interval polling after repeated failures, so a network blip degrades to slightly staler flags rather than to defaults.
6. Flush events and shut down cleanly
The SDK batches analytics events, the exposure data behind a percentage rollout and any track() calls you make, and flushes them on an interval. A process that exits without closing the client loses whatever is still in the current batch. On a server that redeploys often, that is a steady trickle of dropped events.
Close the client on the termination signal:
process.on('SIGTERM', async () => { await flags.close(); process.exit(0);});close() flushes buffered events, closes the SSE connection, and stops the background timers. Under the refcount model, the shared core only runs its real shutdown when the last handle for that SDK key closes, so closing your one shared client on SIGTERM is exactly right. Calling close() twice is a no-op, so a defensive double-close in a shutdown handler does no harm.
7. Test flag-gated code without a network
Flag-gated branches need tests on both sides, and you do not want those tests reaching the network or standing up a server. FeatureflipClient.forTesting() returns a standalone client with hardcoded values, registered nowhere in the factory cache and opening no connections:
import { FeatureflipClient } from '@featureflip/node';
const flags = FeatureflipClient.forTesting({ 'new-dashboard-nav': true,});
flags.boolVariation('new-dashboard-nav', {}, false); // trueInject the test client wherever your code expects the real one and assert on each branch independently. Because the client is fully initialized the moment it is created and ignores the evaluation context, there is no init step to await and no fetch to mock.
8. Why a first-party SDK, and what it meters
You can skip the SDK and call a flag endpoint over HTTP, or poll it and cache the result yourself. That works until the endpoint is slow or down, at which point every request that checks a flag is waiting on a network call or handling its failure, and you are now maintaining the cache, the refresh loop, and the reconnection logic by hand. Local evaluation is that machinery, built and kept in sync with the dashboard for you, which is the kind of undifferentiated plumbing a build-versus-buy decision usually settles in favor of buying.
Pricing matters for a backend in a specific way. A server evaluates flags once per request, so on a platform that bills by evaluation or by monthly active user, your flag bill tracks your traffic, and a launch that triples requests triples the meter. Featureflip uses flat pricing with unlimited evaluations on every plan, so request volume never moves the invoice. The pricing page has the numbers, and the comparison pages show how that lands against per-MAU and usage-billed vendors for a high-traffic service.
The shorter version
What makes feature flags in Node.js different from a script or a browser is the long-lived process. Install @featureflip/node and create one client with FeatureflipClient.get({ sdkKey }) in a shared module, so the whole process uses a single streaming connection and in-memory store. Await waitForInitialization() before app.listen() and surface isInitialized on a readiness probe, so the server never answers a request before flags have loaded. Evaluate with boolVariation('key', context, default), which is a synchronous in-memory read with no network round trip, safe to call anywhere on the hot path. Across replicas, each instance streams config independently and buckets users deterministically, so a dashboard flip reaches the whole fleet in milliseconds with no redeploy. Close the client on SIGTERM to flush analytics, and use FeatureflipClient.forTesting() to cover both branches of a flag without a network.
Frequently asked questions
How do I add feature flags to a Node.js app?
Install @featureflip/node and create one client in a shared module with FeatureflipClient.get({ sdkKey: process.env.FEATUREFLIP_SDK_KEY }). Await flags.waitForInitialization() before your server starts listening, then read flags anywhere with flags.boolVariation('your-flag', { user_id }, false). Evaluation is a synchronous in-memory read, so a flag check adds no latency to a request. The Node.js quickstart covers the minimal setup and the SDK reference covers every option.
Does evaluating a feature flag in Node.js make a network call?
No. The Node SDK fetches your full ruleset once at startup and keeps it current over a streaming connection, then evaluates every flag locally from memory. A boolVariation() call costs about as much as a Map.get, with no per-request round trip to Featureflip and no failure path on the variation call itself. The only network activity is the initial load and the background stream, both of which happen outside your request handlers.
How many Featureflip clients should I create in one Node.js process?
One. FeatureflipClient.get() refcounts by SDK key, so every call with the same key returns a handle backed by a single shared core, one SSE connection, one event-flush loop, one in-memory store. Create the client once in a shared module and import it everywhere, including from per-request handlers and DI containers. Avoid constructing a client per request, which is why the SDK has no public constructor.
How do I avoid serving default flag values when my Node.js server starts up?
FeatureflipClient.get() returns before flags have loaded, so await flags.waitForInitialization() before calling app.listen(), and expose flags.isInitialized on a readiness endpoint like /readyz. Awaiting init means the process does not accept traffic until the ruleset is in memory, and the readiness probe keeps an orchestrator from routing requests to a pod that is still booting. Together they close the window where a flag check would return its default.
How do feature flags stay consistent across multiple Node.js instances?
Each instance holds its own in-memory ruleset and its own streaming connection, and the same user_id hashes to the same bucket on every instance, so a percentage rollout serves the same users the same variation regardless of which replica answers a request. You do not need sticky sessions. A flag flipped in the dashboard pushes to every instance over its stream within milliseconds, so the whole fleet changes together without a redeploy or a shared cache to invalidate.
Featureflip is a focused, flat-priced feature flag platform with a first-party Node SDK that evaluates flags locally in memory, so a Node.js backend gets fast flag checks on the hot path, fleet-wide updates over streaming, and no bill that scales with request volume. The Node SDK reference has the full API, the companion guide on feature flags in Next.js covers the server-render case, adding flags to a Go service applies the same local-evaluation pattern to a static Go binary, and you can start on the free Solo plan without a credit card.