Skip to content

Live configuration and feature flags ​

Scenario: an admin changes a feature flag or a setting (a rate limit, a maintenance banner), and every running instance of every service must apply it within moments, without a restart. Instances that start later must get the current value too.

The configuration is published as a retained message: subscribers to the exact topic get the current value as soon as they subscribe, then every change.

Every instance ​

typescript
import { Relay } from 'bullmq';

const relay = new Relay({ connection: { host: 'redis.internal', port: 6379 } });

let flags: FeatureFlags = DEFAULT_FLAGS;
let flagsGeneration = 0;

async function reloadFlags() {
  const generation = ++flagsGeneration;
  const loaded = await db.settings.get('feature-flags');
  // Ignore reads superseded by a message or a newer reload.
  if (generation === flagsGeneration) {
    flags = loaded;
  }
}

async function loadFlags() {
  let received = false;
  await relay.subscribe('config.feature-flags', message => {
    received = true;
    flagsGeneration++;
    flags = message.data as FeatureFlags;
  });
  // The retained value, if any, is delivered before subscribe() resolves.
  if (!received) {
    await reloadFlags();
  }
}

// Retained messages are not delivered again after a recovery.
relay.on('recovered', () => {
  void reloadFlags().catch(console.error);
});

await loadFlags();

export const isEnabled = (flag: keyof FeatureFlags) => flags[flag];

Admin service ​

typescript
const MONTH = 30 * 24 * 60 * 60 * 1000;

export async function setFlags(next: FeatureFlags) {
  await db.settings.set('feature-flags', next); // source of truth
  await relay.publish('config.feature-flags', next, { retain: MONTH });
}

To keep the retained value from ever expiring, republish it periodically, for example with a job scheduler:

typescript
await queue.upsertJobScheduler('republish-flags', {
  every: 24 * 60 * 60 * 1000,
});

new Worker(
  queue.name,
  async () => {
    const current = await db.settings.get('feature-flags');
    await relay.publish('config.feature-flags', current, { retain: MONTH });
  },
  { connection },
);

Why the Relay ​

  • New instances get the current configuration in the same call that subscribes them to changes, atomically, so no change can be missed between reading and subscribing.
  • Changes reach every instance at once, including instances that briefly lost their connection.

Things to watch ​

  • Keep the database as the source of truth, and the retained message as a fast, pushed copy of it.
  • Use one topic per independent setting (config.feature-flags, config.maintenance) so services only receive what they use.

Released under the MIT License.