SignaKitdocs
Concepts

Feature Flags: What They Are and How They Work

Learn what feature flags are, why engineering teams use them, and how SignaKit evaluates flags locally for sub-millisecond decisions.

Feature Flags

What is a Feature Flag?

A feature flag (also called a feature toggle or feature switch) is a software development technique that lets you change your application's behavior at runtime without deploying new code. Instead of shipping a feature to all users at once, you wrap it in a conditional check — a flag — and control who sees it from a dashboard.

Engineering teams use feature flags to decouple deployment from release: you can merge and ship code to production while keeping a feature off, then gradually enable it for 1%, 10%, or 100% of users. When something goes wrong, you flip the flag off and the problem disappears instantly — no hotfix, no rollback, no deployment.

Common use cases include gradual rollouts, A/B testing (showing different experiences to different user segments and measuring which performs better), kill switches for risky features, and beta access programs where early adopters opt into new functionality before general availability.

In SignaKit, a feature flag is a named boolean gate — and optionally a variation and set of variables — that your code checks at runtime to decide what behavior to show a user. SignaKit evaluates flags entirely in memory, so every decide() call is a fast, synchronous operation with no network round-trip.


How Does SignaKit Evaluate Feature Flags?

SignaKit uses a fetch-once, evaluate-locally model.

createInstance({ sdkKey })
  → fetches full flag config from SignaKit CDN (once)
  → stores config in memory

userCtx.decide(flagKey)
  → reads config from memory
  → evaluates targeting rules locally
  → fires $exposure event asynchronously (first call only)
  → returns decision object

On startup, the SDK fetches the complete flag configuration for your project from the SignaKit CDN and holds it in memory. There is no further network activity during flag evaluation.

On each decide() call, the SDK runs targeting rules against the user context you provide. The result is computed locally from the in-memory config — no HTTP request, no latency overhead, no external dependency at evaluation time.

Because config is fetched once at startup, initialize the client at module level (or application startup) — not on every request. Call await client.onReady() before evaluating flags to ensure the config has loaded.


Initializing the client

lib/signakit.ts
import { createInstance } from '@signakit/flags-node'

export const client = createInstance({ sdkKey: process.env.SIGNAKIT_SDK_KEY! })
await client.onReady()

createInstance accepts your SDK key and begins fetching config immediately. onReady() resolves when the config is loaded and the client is ready to evaluate flags.


The decision object

Create a user context, then call decide() with the flag key:

Evaluating a flag
const userCtx = client.createUserContext('user-123', {
  plan: 'pro',
  country: 'US',
})

const decision = userCtx.decide('checkout-redesign')

decide() returns a decision object, or null if the flag key does not exist in your project:

type Decision = {
  enabled: boolean
  variationKey: string
  variables: Record<string, unknown>
} | null

enabled

The primary boolean gate. Check this first. When false, serve your default / existing behavior. When true, the flag is active for this user.

if (decision?.enabled) {
  // show the new checkout experience
}

variationKey

Identifies which variation the user is assigned to. Common values are 'control' and 'treatment', but you define the variation keys in the dashboard.

Useful for A/B tests where you need to branch on more than two states:

switch (decision?.variationKey) {
  case 'control':
    return renderOriginal()
  case 'treatment-a':
    return renderVariantA()
  case 'treatment-b':
    return renderVariantB()
}

variables

A key/value store attached to the flag. Supports strings, numbers, booleans, and JSON objects. Use variables to ship configuration alongside the flag — no code change needed to adjust a value.

const decision = userCtx.decide('banner')

// Variables defined in the dashboard
const title = decision?.variables.title as string        // "Try our new feature"
const maxItems = decision?.variables.maxItems as number  // 5
const showBadge = decision?.variables.showBadge as boolean // true

variables is always present on the decision object when enabled is true. It will be an empty object if no variables are configured for the flag.


Flag states

A flag in SignaKit is always in one of three states:

Offdecide() returns { enabled: false, variationKey: 'off', variables: {} } for all users. This is the default for a newly created flag.

Ondecide() returns enabled: true for all users, regardless of attributes. Use this to fully roll out a feature.

Targeted — targeting rules are evaluated against the user's attributes and the flag's audience configuration. Some users get enabled: true; others get enabled: false. This is the standard operating mode for gradual rollouts, percentage splits, and A/B tests.

Flags default to off. A flag you just created will return enabled: false until you explicitly turn it on or add targeting rules in the dashboard.


How Does Exposure Tracking Work?

When decide() is called for the first time for a given user and flag combination, the SDK automatically fires an $exposure event. This happens asynchronously in the background — it does not block the return value or add latency to your code path.

Exposures populate the Exposures count in the dashboard. They represent a real user encountering a flag evaluation, making them the source of truth for experiment participation.

First call:  userCtx.decide('checkout-redesign')
             → evaluates locally (sync, in-memory)
             → fires $exposure event (async, non-blocking)
             → returns decision

Subsequent:  userCtx.decide('checkout-redesign')
             → evaluates locally (sync, in-memory)
             → no $exposure fired (already recorded for this context)
             → returns decision

You do not need to instrument exposures manually. As long as you call decide() at the point in your code where the user actually experiences the flag, the exposure is recorded correctly.

Exposure events are batched and sent to the SignaKit event ingestion endpoint. They do not slow down flag evaluation — the evaluation itself is always a pure in-memory operation.


Frequently Asked Questions

Does decide() make a network request?

No. decide() is a pure in-memory operation. The SDK fetches the flag configuration once when you call onReady(), stores it in memory, and all subsequent decide() calls read from that in-memory cache. There is no network activity on the evaluation path.

What happens if a flag key doesn't exist?

decide() returns null. Treat null as the default/off state — your code should always handle this case: if (decision?.enabled) { ... }. If you are seeing unexpected null returns, check that the flag key in your code exactly matches the key in the SignaKit dashboard (keys are case-sensitive).

How quickly do flag changes propagate to the SDK?

Changes appear on the next SDK initialization or config refresh. Long-lived server processes hold the config in memory for the process lifetime by default — call client.refresh() to pull the latest config without restarting. Browser and mobile SDKs refresh on page load or app launch. For fastest propagation in production, configure a polling interval on initialization.

Can I use feature flags in serverless functions?

Yes, with one important rule: initialize the client at module level, not inside the handler. Module-level initialization runs once per container and is reused across warm invocations. Initializing inside the handler creates a new client on every request, which is expensive and defeats the config-caching benefit. See the Troubleshooting page for serverless-specific patterns.


  • Targeted Delivery — how audience rules and percentage splits control which users see a flag enabled
  • A/B Testing — using variationKey to run controlled experiments
  • SDK Architecture — the full lifecycle from config fetch to event pipeline
  • Environments — separating development and production flag state
  • Node.js SDK — full API reference for the server-side SDK

Last updated on

On this page