Feature Flags For Solo Developers in 2026: When You Need Them, When You Do Not, And What I Actually Use

The first feature flag I ever shipped was a single boolean in a YAML config file. The second one was a boolean in a database row. The third one was a third-party feature flag service I spent forty minutes setting up because a tutorial said I should. The third one was a mistake. I deleted the integration two weeks later and went back to the database row.

If you are a solo developer or a two-person team building a real product, you have probably seen the same tutorials I did. They tell you that feature flags are the modern way to deploy safely. They show you LaunchDarkly, Statsig, Flagsmith, ConfigCat. They start at fifty dollars a month and scale up fast. The pitch is targeted at companies with a hundred engineers running thousands of experiments. You are not that company. You may never be that company. And the truth nobody tells you is that you do not need a feature flag service to ship safely. You need to know when a flag is actually earning its keep and when it is just ceremony.

This post is the version of the feature flag conversation I wish someone had had with me. It covers what feature flags actually do, when a config file is enough, the three lightweight setups that earn their place at solo and small-team scale, and the moment you finally graduate to a proper service.


What Feature Flags Actually Do

A feature flag is a runtime switch that changes application behaviour without redeploying. That is the whole definition. Everything else is a feature on top of the switch.

The switches come in four flavours, ordered by complexity:

The first is a release toggle. You merge code that is not ready for users yet, but you put it behind a flag that is off by default. When the code is ready, you flip the flag on. This decouples “the code is in main” from “the feature is live.” The flag is binary, the audience is everyone, and the flag goes away once the feature ships.

The second is a kill switch. You ship a feature, something goes wrong, and you flip the flag off to instantly disable the feature without deploying. The flag stays in the codebase forever as a panic button. Most features that touch payments, file uploads, or third-party APIs need a kill switch.

The third is a gradual rollout. You enable a feature for ten percent of users, watch the metrics, ramp to twenty-five percent, fifty, one hundred. If something breaks, you ramp back down. This is where the targeting logic starts to matter, because “ten percent of users” needs to be a stable subset, not a random ten percent on every request.

The fourth is an experiment. Two variants, randomised assignment, conversion tracking, statistical analysis. This is what feature flag companies actually sell. It is a tiny fraction of what most products need.

For a solo developer, the first three matter. The fourth is almost never worth the overhead until you have enough traffic that statistical tests actually converge, which is roughly when you have something like ten thousand monthly active users. Before that point, you are looking at noise.


When You Do Not Need A Flag

Most code does not need a flag. The instinct to flag everything is the same instinct that leads to over-engineered codebases full of abstractions for hypothetical futures. Flags add cognitive overhead because every branch is now two branches the reader has to track. Old flags that nobody removed are dead weight that gets in the way of every refactor.

Skip the flag if:

The feature is small and contained. A UI tweak, a copy change, a small bug fix. Deploy it. If it breaks, deploy a fix or a revert. The flag costs more than the protection it provides.

The feature is part of a larger flow you cannot ship partially. If turning the flag off would leave the product in a broken state, the flag is not protecting you, it is hiding a coupling.

The change is irreversible. A database migration, a webhook reconfiguration, a third-party API switch. Flags cannot undo state changes. They can only prevent state changes from happening in the first place, and only if the flag is checked before the change.

You are pre-launch. If nobody is using the product, there is nothing to break. Flags exist to protect users, not to protect future users. Ship without flags until you have real users to disappoint.

Most solo projects spend their first six to twelve months in the “no users yet” or “few users” stage. During that stage, the right deploy strategy is to ship fast, watch for errors, revert if needed. Flags add overhead with little payoff.

The signal that you should start using flags is the first time you ship a change and immediately wish you could undo it without a redeploy. That is the moment a kill switch would have helped. That is the moment to start.


When You Actually Need A Flag

The use cases where flags genuinely pay off, even at small scale:

Anything that touches money. Payment flows, subscription changes, refund logic, pricing tiers. The cost of a broken deploy is real money lost or refunded. A kill switch on the critical path is cheap insurance. I have one on every Stripe-related path in my products. It has fired twice in two years. Both times saved me an evening of customer support.

Anything that calls a third-party API on the critical path. Email sending, SMS, image processing, anything that costs money per call and has rate limits. If the API misbehaves or your usage spikes, you want to disable the integration without redeploying.

Anything that is rolling out to a specific user segment. Beta features for paying users, admin-only tools, region-specific behaviour. The flag is doing real work of routing different users to different code paths.

Anything you would otherwise gate by environment variable. A flag service or database row is just a better-shaped environment variable for things you want to change without a deploy. The env vars in production article touches on this in a different context, but the principle is the same: anything you want to flip at runtime should not live in your bundled config.

Pre-launch dark launches. You merge the new feature with the flag off, you flip the flag on for your own account, you test in production with real data, you flip on for a few users, you ramp up. This pattern works regardless of team size. A solo developer can dark-launch by treating themselves as user-one.

If none of those apply, you do not need a flag. If at least one does, you need a flag for that specific feature, not a flag system across your whole codebase.


The Three Setups That Cover Solo And Small-Team Scale

Here are the three feature flag setups I have actually used in production, in increasing order of complexity. Pick the smallest one that solves your current problem.

Setup One: The Config File

The simplest possible feature flag is a constant in code. Some teams call this anti-pattern. I call it the right answer for the first few flags.

export const flags = {
  newCheckoutFlow: false,
  exportPdfV2: false,
  betaAiSummariser: true,
};

The flag is checked at runtime. Flipping it requires a deploy. The deploy is the whole point: the change is recorded in git, reviewed in a PR, and rolled back by reverting the commit.

This works when:

  • You can deploy quickly (under five minutes from “flip the flag” to “users see the change”).
  • You do not need to roll out to a subset of users.
  • You do not need to disable the feature without a deploy.

It does not work when you need a kill switch, because by the time you have edited the config and pushed a deploy, the broken feature has already harmed your users. For everything else, this is enough. Most of the flags I have ever shipped have been this shape.

The graduation signal is the first time you wish you could disable something without a deploy. Move to Setup Two.

Setup Two: The Database Row

A single table of flags, read at request time, with a small layer of caching to avoid querying on every request:

CREATE TABLE feature_flags (
  key TEXT PRIMARY KEY,
  enabled BOOLEAN NOT NULL DEFAULT false,
  enabled_for_user_ids JSONB DEFAULT '[]'::jsonb,
  enabled_percentage INTEGER DEFAULT 0,
  updated_at TIMESTAMPTZ DEFAULT now()
);

Application code that reads the flag:

async function isEnabled(key: string, userId?: string): Promise<boolean> {
  const flag = await getCachedFlag(key);
  if (!flag) return false;
  if (flag.enabled) return true;
  if (userId && flag.enabled_for_user_ids.includes(userId)) return true;
  if (userId && stableBucket(userId, key) < flag.enabled_percentage) return true;
  return false;
}

The stableBucket function hashes the user ID with the flag key and returns a number from zero to ninety-nine. The hash is deterministic, so the same user always lands in the same bucket for the same flag. A percentage rollout uses the bucket: users in buckets zero through twenty-four get the feature when the rollout is at twenty-five percent.

function stableBucket(userId: string, key: string): number {
  const hash = createHash('sha256').update(userId + key).digest();
  return hash.readUInt32BE(0) % 100;
}

Cache reads aggressively. Most flags do not change minute-to-minute, so a thirty-second in-process cache is fine. Some teams use a Redis layer for sub-second propagation across nodes. For a solo product running on a single VM or a serverless platform with a small number of instances, in-process is enough.

Build a tiny admin page (or a CLI command) to flip flags. Two endpoints: list flags, update flag. Restrict access to your own admin account. The admin page does not need to be pretty. It needs to work in thirty seconds when something is on fire.

This setup gives you kill switches, percentage rollouts, per-user enabling, and same-database transactional safety. It costs zero dollars in marginal infrastructure if you already have a database. It scales to thousands of flags before performance becomes an issue.

The graduation signal is when you want a flag to be the same across multiple unrelated apps, or when you need targeting more sophisticated than user-ID-and-percentage, or when you want non-engineers to manage flags. Move to Setup Three.

Setup Three: A Hosted Service

When you cross the line where Setup Two stops paying off, the move is to a hosted feature flag service. The market in 2026 is broad. The realistic options for small teams:

ConfigCat is the cheapest option for small teams that want a managed dashboard. The free tier covers most solo projects (ten flags, two environments, sub-second propagation). Pricing scales by request volume, not seat count, which fits indie hackers better than per-seat pricing.

Flagsmith has a generous self-hosted open-source mode. If you want a real dashboard but you do not want to pay, you can host it yourself for free on the same VM your app runs on. The self-hosted path is genuinely production-ready, not a stripped-down version.

Statsig is more focused on experimentation and analytics. It is the right move if you actually need A/B testing with statistical confidence, not just toggles. The free tier is generous (a million events a month) but the value of the platform is in the analytics, which only matter if you have enough traffic for statistical significance.

Unleash is the open-source veteran. Mature, well-documented, self-hostable, and the SDKs are widely used. It is what I would pick if I were running a small team that needs the full feature flag feature set without paying for it.

Skip LaunchDarkly until you are a real company with real budget. Their product is fine, their pricing is not designed for solo developers, and the feature gap between LaunchDarkly and ConfigCat at small scale is not worth the difference in cost.

The criteria for picking among the lightweight options:

  • If you want zero infrastructure and pay-as-you-go pricing: ConfigCat.
  • If you want self-hosting and a real dashboard: Flagsmith or Unleash.
  • If you actually need experimentation analytics: Statsig.

For most solo products that need flags, ConfigCat or self-hosted Unleash is the answer. The decision is reversible: the SDK abstraction is similar across providers, and migrating from one to another is a couple of hours if you wrote your flag-check function as a thin wrapper around the provider’s SDK.


The Wrapper That Makes Migration Easy

The biggest mistake I see people make with feature flag services is to call the provider SDK directly from every place in the codebase. Then when you want to swap providers, every flag check has to change.

The fix is a one-function abstraction:

// flags.ts
import { configcat } from './providers/configcat';

export async function isEnabled(
  key: string,
  context?: { userId?: string }
): Promise<boolean> {
  return configcat.isEnabled(key, context);
}

Every flag check in your app calls isEnabled. The provider is hidden behind a single function. Swapping providers is changing one file.

This wrapper is also where you put your test overrides. In tests, you replace the provider with a static map. In development, you can read from a local YAML file. In production, you hit the real service. The wrapper handles the dispatch.

const overrides = process.env.FLAG_OVERRIDES
  ? JSON.parse(process.env.FLAG_OVERRIDES)
  : null;

export async function isEnabled(key: string, context?: any) {
  if (overrides && key in overrides) return overrides[key];
  return provider.isEnabled(key, context);
}

The override layer is what lets you debug “why is this flag not firing” without poking around in a third-party dashboard. Set FLAG_OVERRIDES={"newCheckout":true} in your local env, restart, the flag is on for you regardless of the dashboard state.


The Hygiene That Keeps Flag Sprawl In Check

Every feature flag is a code branch the reader of your code has to track. Old flags rot. They survive past the feature they were gating, they get checked in dead code paths, and eventually a new engineer asks “what does this flag even do” and nobody knows.

The hygiene that prevents this:

Every flag has a sunset date in a comment when you add it. Six months out by default, less for release toggles. When the date hits, the flag either gets removed or its date gets extended with a written reason.

// FLAG: newCheckoutFlow
// Added: 2026-03-12
// Sunset: 2026-09-12
// Owner: alex
// Purpose: gradual rollout of new Stripe Checkout integration
if (await isEnabled('newCheckoutFlow', { userId })) {
  return renderNewCheckout();
}
return renderLegacyCheckout();

Release toggles get cleaned up after the feature ships. Kill switches stay forever, but they are explicitly labelled as kill switches and not as release toggles.

A monthly cron job (or a calendar reminder for solo devs) to grep the codebase for flag names that no longer exist in the flag store, or flags in the flag store that are not checked anywhere. Both are signs of drift.

The flag table or service is the source of truth for what flags exist. If you check isEnabled('foo') and foo does not exist in the store, the result should be a logged warning, not a silent false. Silent false is how flag drift bugs hide.

Treat the flag list like the dependency list. Audit it. Prune it. A flag that has been on for six months everywhere is not a flag, it is dead code with a runtime branch.


The Pattern For Rolling Out Without Pain

When you are ready to enable a feature, the pattern that works is the same regardless of which setup you are on:

Start by enabling for yourself only. Use the per-user-ID list. Verify the feature works in production with your real data.

Enable for a single friendly user. Someone who knows you are testing and will tell you if something breaks.

Enable for one percent. Watch error rates, latency, conversion. Watch for a full business day.

Enable for ten percent. Watch again. The metrics you care about most depend on the feature: error rate for technical reliability, conversion or retention for product impact.

Enable for fifty percent. By now you have enough data that something has shown up if it was going to.

Enable for one hundred. Leave the flag in for a couple of weeks. If anything goes wrong, you can roll back to zero without redeploying.

After two weeks of clean operation at one hundred percent, remove the flag from the code. The feature is now permanent.

The whole arc takes one to three weeks for a non-trivial feature. The pace is set by how much traffic you have. With a hundred users a day, a percentage rollout takes longer to gather meaningful signal than with a million. At very small scale, the percentage rollout is mostly ceremony, and you should focus on the per-user-ID enabling instead.


Common Mistakes That Hurt

The mistakes I made or watched others make, in rough order of pain:

Putting flag checks inside hot loops. Every flag check is a network call or at least a cache lookup. If you check the same flag a thousand times in a request, that is a thousand checks. Cache the result for the request. Most flag libraries do this automatically; your custom wrapper should too.

Forgetting to handle the flag-not-found case. If your flag service is down or the flag does not exist, the default behaviour should be the safe one. For a release toggle, safe is false (do not show the new thing). For a kill switch, safe might be true (the feature is on, do not disable it). Think about what “safe” means for each flag explicitly.

Using flags for things that should be database columns. “Is this user on the pro plan” is not a feature flag, it is a user state. Storing it as a flag means you have to keep the flag service in sync with the database. Just check the database. Flags are for code paths, not user attributes.

Building targeting rules in the dashboard that nobody can read. A flag that is enabled for “users in North America with more than ten projects created in the last thirty days who have not seen the welcome email” is a flag that nobody will understand six months later. If the targeting is complex, the targeting belongs in code, not in the flag service. Keep the flag itself simple.

Treating flag changes as harmless. Flipping a flag in production is a deploy in disguise. It changes the running behaviour of your app. Log it. Track who did it. Be able to ask “what changed in the last hour” and have the answer include flag flips.


The Honest Cost Picture

Here is what feature flag tooling actually costs at solo and small-team scale in 2026.

A config-file setup is free. Cost is one minute per flag, the minute it takes to add the boolean.

A database-row setup is free, assuming you have a database. Cost is a few hours to build the table, the wrapper, the admin page, and the bucketing function. Then a minute per flag.

ConfigCat at small scale: free for the first ten flags, then around forty dollars a month for the next tier. Pricing scales by request volume, which for a solo product is usually fine for months or years.

Statsig at small scale: free up to a million events a month, which covers most products until you have real traffic.

Self-hosted Unleash or Flagsmith: the marginal cost of running another container on your VM. Twenty dollars a month of VM, less if you are already running other services on the same box.

LaunchDarkly: starts at a few hundred dollars a month, scales fast. Worth it for real engineering teams. Not worth it for solo developers.

The honest math: for the first hundred users, the config file is fine. For the first thousand users, the database row covers it. After that, a hosted service starts paying off because the admin UI saves time. The right time to upgrade is when you find yourself in the database every week to flip flags and you wish you had a dashboard. Not before.


What I Run In Production

For my main product, which has a few thousand monthly active users, the setup is:

The flag store is a Postgres table called feature_flags. The schema is the one above. Updates are made through a small admin page I built into the product itself, accessible only to my admin user. The page lists every flag, shows its current state, and has a toggle. Updates go through a Postgres transaction that also writes an audit log row. The audit log captures who changed what, when, and why.

The flag check function lives in src/lib/flags.ts. It is the wrapper. Every flag check in the app goes through that function. The function caches results for thirty seconds in process. On serverless, the cache lifetime is the function instance’s lifetime, which is usually a few minutes.

Each flag has a comment in code with the sunset date, the owner (me), and the purpose. A monthly calendar reminder makes me look at the flag list and prune anything that has been on for six months. The list has about twenty flags right now. Half are kill switches that will never go away. The other half are release toggles in various states.

For one product where I have a co-founder and we are running A/B tests, I added Statsig on top of the Postgres flags. The Postgres flags handle release toggles and kill switches. Statsig handles the experiment variants. The wrapper in flags.ts dispatches to the right backend based on the flag name’s prefix.

That is the whole setup. No third-party feature flag service for the main product. Statsig only where the analytics earn their place. Total cost: zero dollars a month for the flag infrastructure, plus the Statsig free tier for the experiments. Total complexity: one table, one wrapper, one admin page.


What I Would Tell You If You Asked

You probably do not need a feature flag service. You probably do not need a feature flag system. You probably need a kill switch on your Stripe webhook, a release toggle for the next big feature, and a per-user enable for your own account so you can dark-launch.

The mistake is to start with the service and let it dictate how you think about flags. The right move is to start with the smallest thing that solves the actual problem, and graduate only when the smallest thing stops working. The graduation signals are concrete: “I need to flip this without a deploy” moves you from config file to database. “I need targeting more complex than user-and-percentage” moves you from database to service. “I need experimentation analytics” moves you to a service with analytics, not a service with just flags.

The bigger pattern, the one that generalises beyond feature flags, is that solo developers and small teams should use the smallest tool that works and resist the pressure to adopt the tools that are built for hundred-engineer teams. The same instinct that says “just use a config file” for flags is the same instinct that says “pick the boring stack” for everything else. The complexity of your tooling should scale with the complexity of your problem, not with the LinkedIn job titles of the people writing tutorials.

Flags are a tool. The tool earns its place when you have a specific problem it solves. Until then, the tool is overhead. Ship without flags until you have the specific problem. Then ship the smallest version of the flag system that solves it. Then graduate when the small version stops fitting.

The product I am working on right now has twenty active flags and zero feature flag service. The cost is twenty seconds when I add a flag and a few cents per month in database load. The protection is real. The complexity is small. That is the whole point.

Use flags when they earn their keep. Resist them when they do not. The smaller your team, the more important it is to know the difference.