Configure the CMAB cache for the Javascript SDK
How to configure Contextual Multi-Armed Bandit (CMAB) settings in the Feature Experimentation JavaScript SDK, including caching behavior and prediction endpoint customization.
BetaCMAB for Feature Experimentation is in beta. Contact your Customer Success Manager for more information.
The CMAB cache stores variation assignments to reduce latency and minimize API calls to the CMAB service.
NoteThis document uses the terms
Contextual Multi Armed Bandit (CMAB)andContextual Banditsinterchangeably.
Prerequisites
- JavaScript SDK version 6.2.0 or higher.
- A CMAB-enabled experiment in your Optimizely Feature Experimentation project.
Minimum SDK version
6.2.0
Description
You can create Contextual Bandit rules for flags in your Optimizely Feature Experimentation project. To get decisions for CMAB rules, you must use the asynchronous version of decide methods, namely decideAsync, decideForKeysAsync and decideAllAsync. Sync decide methods (decide, decideForKeys, decideAll do not support CMAB rules and skip them.
When a user is bucketed into a CMAB experiment, the JavaScript SDK makes an API call to the CMAB service to determine which variation to show. To improve performance and reduce latency, the SDK caches these decisions based on the following:
- User ID.
- Experiment ID.
- CMAB attribute values.
The cache is automatically invalidated when CMAB attributes change for a user, ensuring fresh decisions when context changes.
By default, the SDK uses an in-memory Least Recently Used (LRU) cache with a maximum size of 10000 entries and a TTL of 30 minutes. You can customize these settings or provide your own cache implementation.
Parameters
Configure CMAB caching by passing a configuration object for the cmab option when creating your Optimizely client using createInstance. The cmab config object can have the following parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
cacheTtl | number | No | 30 * 60 * 1000 (30 minutes) | How long cache entries remain valid (in milliseconds). |
cacheSize | number | No | 10000 | Maximum number of decisions to cache. |
cache | CacheWithRemove<string> | No | Custom cache implementation to override the default in memory LRU cache. |
Example
Basic configuration
Adjust cache size and TTL for your application's needs, like in the following example:
import {
createBatchEventProcessor,
createInstance,
createPollingProjectConfigManager,
} from "@optimizely/optimizely-sdk";
const SDK_KEY="YOUR_SDK_KEY"; //Replace with your SDK key.
const pollingConfigManager = createPollingProjectConfigManager({
sdkKey: SDK_KEY,
autoUpdate: true,
});
const batchEventProcessor = createBatchEventProcessor();
const optimizelyClient = createInstance({
projectConfigManager: pollingConfigManager,
eventProcessor: batchEventProcessor,
cmab: {
cacheSize: 20_000, // cache 20K decisions
cacheTtl: 60 * 60 * 1000, // cache for 60 minutes
}
});
Advanced: Custom cache implementation
You can provide your own cache implementation that satisfies the CacheWithRemove<string> interface.
type CacheWithRemove<V> = SyncCacheWithRemove<V> | AsyncCacheWithRemove;
interface SyncCacheWithRemove<V> {
operation: 'sync';
save(key: string, value: V): unknown;
lookup(key: string): V | undefined;
reset(): unknown;
remove(key: string): unknown;
}
interface AsyncCacheWithRemove<V> {
operation: 'async';
save(key: string, value: V): Promise<unknown>;
lookup(key: string): Promise<V | undefined>;
reset(): Promise<unknown>;
remove(key: string): Promise<unknown>;
}
You can either provide a synchronous or an asynchronous cache. The following examples use a custom cache:
import {
createBatchEventProcessor,
createInstance,
createPollingProjectConfigManager,
} from "@optimizely/optimizely-sdk";
const SDK_KEY="YOUR_SDK_KEY";
// Example sync cache
class CustomSyncCache {
constructor() {
this.operation = 'sync';
this.cache = new Map();
}
save(key, value) {
this.cache.set(key, value);
}
lookup(key) {
return this.cache.get(key);
}
remove(key) {
return this.cache.delete(key);
}
reset() {
this.cache.clear();
}
}
class CustomAsyncCache {
constructor() {
this.operation = 'async';
this.cache = new Map();
}
async save(key, value) {
await new Promise(resolve => setTimeout(resolve, 10));
this.cache.set(key, value);
}
async lookup(key) {
await new Promise(resolve => setTimeout(resolve, 10));
return this.cache.get(key);
}
async remove(key) {
await new Promise(resolve => setTimeout(resolve, 10));
return this.cache.delete(key);
}
async reset() {
await new Promise(resolve => setTimeout(resolve, 10));
this.cache.clear();
}
}
const optimizelyClientWithSyncCache = createInstance({
projectConfigManager: createPollingProjectConfigManager({
sdkKey: SDK_KEY,
autoUpdate: true,
}),
eventProcessor: createBatchEventProcessor(),
cmab: {
cache: new CustomSyncCache(),
},
});
const optimizelyClientWithAsyncCache = createInstance({
projectConfigManager: createPollingProjectConfigManager({
sdkKey: SDK_KEY,
autoUpdate: true,
}),
eventProcessor: createBatchEventProcessor(),
cmab: {
cache: new CustomAsyncCache(),
},
});Cache behavior
Cache invalidation
The cache is automatically invalidated when
- The cached entry's TTL expires.
- CMAB attribute values change for a user (detected through attribute hash comparison).
- The
INVALIDATE_USER_CMAB_CACHEdecide option is used. - The
RESET_CMAB_CACHEdecide option is used.
CMAB-specific decide options
Control cache behavior on a per-decision basis using decide options, like in the following example:
import {
createBatchEventProcessor,
createInstance,
createPollingProjectConfigManager,
OptimizelyDecideOption
} from "@optimizely/optimizely-sdk";
const SDK_KEY="YOUR_SDK_KEY";
const pollingConfigManager = createPollingProjectConfigManager({
sdkKey: SDK_KEY,
autoUpdate: true,
});
const batchEventProcessor = createBatchEventProcessor();
const optimizelyClient = createInstance({
projectConfigManager: pollingConfigManager,
eventProcessor: batchEventProcessor,
cmab: {
cacheSize: 20_000, // cache 20K decisions
cacheTtl: 60 * 60 * 1000, // cache for 60 minutes
}
});
const user = optimizelyClient.createUserContext("user-id");
// Ignore cache for this decision (always fetch fresh)
let decision = await user.decideAsync("my-flag", [OptimizelyDecideOption.IGNORE_CMAB_CACHE]);
// Invalidate cached decision for this user and experiment
let decision = await user.decideAsync("my-flag", [OptimizelyDecideOption.INVALIDATE_USER_CMAB_CACHE]);
// Clear entire CMAB cache
let decision = await user.decideAsync("my-flag", [OptimizelyDecideOption.RESET_CMAB_CACHE]);The following are the available decide options with a short description of what they do:
OptimizelyDecideOption.IGNORE_CMAB_CACHE– Ignore the cache while deciding CMAB experiments.OptimizelyDecideOption.RESET_CMAB_CACHE– If the user is bucketed into a CMAB experiment, reset the whole CMAB cache before fetching a decision for the CMAB experiment.OptimizelyDecideOption.INVALIDATE_USER_CMAB_CACHE– If the user is bucketed into a CMAB experiment, invalidate the previously cached decision for the experiment and user combination.
When to use custom cache
Consider implementing a custom cache in the following situations:
- Multi-instance deployments – Share cache across multiple application instances.
- Distributed systems – Use external caching systems for centralized caching.
- Persistent cache – Maintain cache across application restarts.
- Custom eviction policies – Implement domain-specific cache management.
- Monitoring – Track cache metrics (hit rate, memory usage, and so on).
Updated about 1 hour ago