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
Freeze the deploys, not the flags
A change freeze stops merges but not the flag dashboard. Freeze anything that adds risk, keep the switches that remove it, and enforce the rule with approvals.
Don't cache the answer to a feature flag
A cached page remembers the first visitor's flag, and your kill switch waits for the TTL. Evaluate per request and put the variant in the cache key.
What to put in the user you pass to a feature flag
The flag context starts as an id and ends up holding emails and names that no rule reads. Use an opaque id, pass only the attributes your rules compare, and build the user in one place.