Feature Flags in Background Jobs: Evaluate at Execution, Not Enqueue
Your web tier checks flags on every request. Your worker checked once — at boot, three deploys ago.
Background jobs break the assumptions that make flags easy in request handlers. A job is enqueued under one flag state and executes under another — minutes later for a queue consumer, days later for a scheduled retry. A worker process runs for weeks, so anything it decided at startup is stale. And when a rollout goes wrong, flipping the flag off stops new web traffic instantly, but a queue with ten thousand pending jobs keeps replaying the old decision.
There's a subtler failure too: jobs often have no obvious user. Evaluate a percentage rollout against the worker's hostname and every job on that box gets the same variant — your 10% rollout becomes 0% or 100% depending on which pod picked up the work.
Three rules for flags in workers
Evaluate when the job runs, not when it's enqueued. The producer shouldn't bake flag decisions into the payload. Evaluate inside the job handler so every execution — including retries — reflects the current rollout state. Snapshot a decision into the payload only when mid-flight consistency matters, like a multi-step email sequence that must not switch templates halfway through.
Key evaluation on the job's subject, not the worker. Use the user the job is about. Featureflow's bucketing is deterministic per user key, so a customer inside the 10% cohort on the web tier lands in the same cohort when their invoice job runs — one consistent experience across both paths.
Give every consumer a kill switch. Long-lived processes shouldn't wait for the next job to notice a change. The Node SDK emits an updated event when feature configuration changes — use it to pause a consumer without a redeploy.
import Featureflow from 'featureflow-node-sdk';
const featureflow = new Featureflow.Client({
apiKey: process.env.FEATUREFLOW_SERVER_KEY,
});
async function processJob(job) {
// Evaluate per execution, keyed on the job's subject
const user = new Featureflow.UserBuilder(job.customerId)
.withAttribute('plan', job.plan)
.build();
if (featureflow.evaluate('new-invoice-renderer', user).isOn()) {
return renderInvoiceV2(job);
}
return renderInvoiceV1(job);
}
// Kill switch: react to flag changes mid-run, no redeploy
featureflow.on('updated', () => {
if (featureflow.evaluate('pause-invoice-worker', 'invoice-worker').isOn()) {
queue.pause();
} else {
queue.resume();
}
});One more safeguard: workers often boot in environments where the first thing to fail is the network. Pre-register features with failover variants so a consumer that can't reach flag config starts in a known-safe state instead of throwing — the Featureflow docs cover this under pre-registering features.
Flags earn their keep in the parts of your system users never see. Request handlers were the easy case — your queues are where rollout state, bucketing keys, and kill switches actually get tested.
#FeatureFlags#BackgroundJobs#MessageQueues#ContinuousDelivery#NodeJS
Put your workers behind flags
Start free with Featureflow — per-job evaluation, deterministic bucketing, and kill switches for every consumer.
Start Now (Free)Related Articles
How Percentage Rollouts Actually Work: Deterministic Bucketing Explained
A 10% rollout isn't a dice roll on every request. Deterministic hashing gives each user a stable bucket — so rollouts are sticky, expanding 10% → 50% keeps the original users, and no assignment table is needed.
Managing Feature Flags Across Environments Without Config Drift
Dev, staging, and prod each need different flag states — but keeping them consistent is where most teams stumble. Here's how to manage multi-environment flag config without drift.
Server-Side vs Client-Side Feature Flags: Choosing the Right Boundary
Same flag, same key — but move a decision from backend-only code to client-visible UI and you change its latency, exposure, and coordination risk. Here's how to use both safely.