Implementing Feature Flags with Unleash in Node.js: Instant Toggles Without Redeploying

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Context & Real-World Challenges

About two years ago, every production release of a new feature was high-stress for our team. The process was typical: merge code into main, trigger CI/CD, build the Docker image, and update the Kubernetes cluster. If a memory leak or a table-locking PostgreSQL query slipped through, the whole team had to scramble to revert commits and rebuild from scratch. This rollback process routinely ate up 15 to 20 minutes. During that window, hundreds of customer orders were disrupted.

The root problem was that we were conflating two distinct concepts:

  • Deployment: Getting new code safely running on servers.
  • Release: Making that feature accessible to end users.

Decoupling these two steps is the exact reason Feature Flags (or Feature Toggles) exist. Instead of hoarding long-lived branches for a month, the five engineers on our team switched to continuously merging into the main branch every day (Trunk-based Development). All new code was wrapped inside a Feature Flag set to OFF. Whenever we wanted to test internally or roll out to 5% of our users, the tech lead simply toggled the flag on the UI. Incident recovery time plummeted from 20 minutes to a single click taking just 1 second.

In terms of tooling, LaunchDarkly runs great, but pricing at $8–$10/user/month gets steep for a startup. Flagsmith is also solid, but I chose Unleash because it is open-source and easily self-hosted via Docker, keeping our internal data fully in-house. The biggest selling point is that Unleash’s Node.js SDK evaluates flags directly in memory, resulting in sub-0.1ms latency per call.

Environment Setup

The system consists of two separate components: the Unleash Server (admin UI and configuration storage) and the Unleash Client SDK embedded directly into your Node.js application.

1. Running Unleash Server with Docker Compose

Here is a production-ready docker-compose.yml file to spin up Unleash alongside PostgreSQL 15:

services:
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: unleash
      POSTGRES_USER: unleash_user
      POSTGRES_PASSWORD: secret_db_password
    volumes:
      - unleash_db_data:/var/lib/postgresql/data
    restart: unless-stopped

  unleash:
    image: unleashorg/unleash-server:latest
    ports:
      - "4242:4242"
    environment:
      DATABASE_URL: postgres://unleash_user:secret_db_password@db:5432/unleash
      DATABASE_SSL: "false"
      LOG_LEVEL: "warn"
      INIT_FRONTEND_API_TOKENS: "default:development.unleash-insecure-frontend-api-token"
      INIT_CLIENT_API_TOKENS: "default:development.unleash-insecure-client-api-token"
    depends_on:
      - db
    restart: unless-stopped

volumes:
  unleash_db_data:

Start the services:

docker compose up -d

After about 15 seconds, open your browser and navigate to http://localhost:4242. Default login credentials:

  • Username: admin
  • Password: unleash4all (you will be prompted to change it immediately upon first login)

2. Installing the SDK in your Node.js Project

Run the following command to install the necessary packages in your Express backend directory:

npm install unleash-client express dotenv

Configuration and Integration

1. Creating an API Token in Unleash

For your Node.js server to fetch flag states, you need an authorized token:

  1. Go to the Unleash dashboard > Settings > API access.
  2. Click New API token.
  3. Select Server-side SDK (Client), assign the default project, and select the development environment.
  4. Copy and save the generated token string.

Next, navigate to Feature toggles and create a new toggle named new-checkout-flow.

2. Initializing Unleash Client as a Singleton

The Unleash SDK operates on an in-memory & background polling model. Every few seconds, the client polls the server in the background to fetch all active rules into local RAM. When your API evaluates a flag, the logic runs entirely against local memory. No outgoing HTTP requests are made during request execution, guaranteeing blazing-fast response times.

Create src/unleash.js:

const { initialize } = require('unleash-client');

let unleashInstance = null;

function getUnleash() {
  if (!unleashInstance) {
    unleashInstance = initialize({
      url: process.env.UNLEASH_URL || 'http://localhost:4242/api/',
      appName: 'payment-service',
      environment: process.env.NODE_ENV || 'development',
      customHeaders: {
        Authorization: process.env.UNLEASH_API_TOKEN || '*:development.unleash-insecure-client-api-token',
      },
      // Sync new rules every 5 seconds in dev (prod should use 10000 - 15000ms)
      refreshInterval: 5000,
      // Send hit/miss metrics to server every 30 seconds
      metricsInterval: 30000,
    });

    unleashInstance.on('ready', () => {
      console.log('[Unleash] SDK successfully loaded rules into RAM.');
    });

    unleashInstance.on('error', (err) => {
      console.error('[Unleash] Configuration sync error:', err.message);
    });
  }
  return unleashInstance;
}

module.exports = { getUnleash };

3. Applying the Flag to a Checkout Endpoint

Below is src/app.js demonstrating how to wrap business logic with a feature flag:

const express = require('express');
const { getUnleash } = require('./unleash');

const app = express();
app.use(express.json());

const unleash = getUnleash();

app.post('/api/checkout', (req, res) => {
  const userId = req.headers['x-user-id'] || 'anonymous';
  const userEmail = req.headers['x-user-email'] || '';

  // Context for Unleash to evaluate targeting rules
  const context = {
    userId: userId,
    properties: {
      email: userEmail,
      plan: req.body.plan || 'free',
    },
  };

  // Check toggle state with a safe fallback of false
  const isNewFlowActive = unleash.isEnabled('new-checkout-flow', context, false);

  if (isNewFlowActive) {
    // New Stripe Elements checkout flow
    return res.json({
      version: 'v2',
      gateway: 'stripe-one-click',
      orderId: 'ORD-2026-NEW',
    });
  }

  // Battle-tested legacy checkout flow
  return res.json({
    version: 'v1',
    gateway: 'standard-redirect',
    orderId: 'ORD-2026-LEGACY',
  });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Backend listening on port ${PORT}`);
});

Notice the third argument in unleash.isEnabled('new-checkout-flow', context, false). This serves as a safe fallback. If the Unleash Server experiences network issues or restarts, the SDK immediately defaults to false. Your application automatically uses the legacy flow without throwing exceptions or crashing the process.

4. Rolling Out Features with Activation Strategies

Unleash goes beyond simple all-or-nothing toggles. Its true strength lies in dynamic activation strategies configured right from the dashboard:

  • Gradual Rollout (Canary testing): Incrementally release flags to 10%, 25%, and eventually 100% of your user base. Unleash’s MurmurHash3 algorithm hashes the userId to guarantee consistent sticky sessions—a user allocated to the 10% bucket will consistently see the new experience on subsequent visits.
  • User ID Targeting: Specify explicit user accounts (e.g., dev_quang, qa_lan) so your internal team can test new flows directly in production.
  • Custom Constraints: Filter by custom conditions, such as corporate email domains matching @company.com or mobile app versions >= 2.4.0.

Testing and Operations

1. Testing Instant Flow Switching

Start the Express server:

node src/app.js

Send a test request while the new-checkout-flow toggle is OFF:

curl -X POST http://localhost:3000/api/checkout \
  -H "Content-Type: application/json" \
  -H "x-user-id: user_123"

The response serves the v1 implementation:

{"version":"v1","gateway":"standard-redirect","orderId":"ORD-2026-LEGACY"}

Now, head over to the Unleash dashboard and switch the toggle to ON. Wait about 5 seconds for the SDK’s polling cycle to refresh, then re-run the same curl command. The response instantly updates to v2:

{"version":"v2","gateway":"stripe-one-click","orderId":"ORD-2026-NEW"}

The entire transition happens seamlessly in the blink of an eye. Zero dropped HTTP connections. Zero container restarts.

2. Monitoring Performance with the Metrics Tab

Every time isEnabled is invoked, the SDK aggregates metrics and periodically reports them to the server. Open your toggle details and switch to the Metrics tab to inspect:

  • The distribution of Yes vs. No evaluations over the past hour.
  • Real-time traffic distribution across both code paths.
  • Early detection of stale flags that have remained at 100% for too long without being cleaned up from the codebase.

3. Three Best Practices to Avoid Technical Debt

After more than 6 months of running Unleash in a production environment handling roughly 50,000 orders per day, our team established three vital rules:

  • Use standardized naming prefixes: Categorize flag intent from day one. Use release- for gradual feature launches, killswitch- for emergency circuit breakers on third-party integrations, and experiment- for short-term A/B tests.
  • Clean up flags aggressively after launch: Feature flags are meant to de-risk releases, but unpruned flags quickly inflate technical debt. Once a feature has run smoothly at 100% for two weeks, immediately log a ticket in the next sprint to remove the if/else branch and archive the toggle in Unleash.
  • Keep flags decoupled: Avoid nesting feature flags whenever possible. Code nested two or three flag levels deep turns unit testing and bug reproduction into a maintenance nightmare.

Thanks to Unleash, shipping new features to production is no longer a high-stakes Friday night gamble. When every potential issue can be neutralized within a single second, the entire engineering team can move faster and ship with confidence.

Share: