Redis-backed implementation of MemberJunction's ILocalStorageProvider interface. Enables persistent, shared server-side caching via any Redis-compatible service — self-hosted Redis, Azure Managed Redis, AWS ElastiCache, Redis Cloud, Upstash, or any other Redis-protocol endpoint.
MemberJunction's default server-side cache (InMemoryLocalStorageProvider) stores data in a plain Map inside the Node.js process. This works well for single-server development but has two limitations in production:
| Limitation | Impact |
|---|---|
| Not shared | Each MJAPI instance has its own cache — no benefit from horizontal scaling |
| Not persistent | Cache is lost on process restart — cold starts hit the database for everything |
A Redis-backed provider solves both problems while remaining a drop-in replacement — no changes to LocalCacheManager, ProviderBase, or any consumer code.
# Add to the package that configures your data provider (typically MJAPI or your server bootstrap)
# Then run npm install at the repo root
npm install @memberjunction/redis-provider
Monorepo note: In the MemberJunction monorepo, add the dependency to the relevant package's
package.jsonand runnpm installat the repo root.
import { RedisLocalStorageProvider } from '@memberjunction/redis-provider';
import { Metadata } from '@memberjunction/core';
import type { GenericDatabaseProvider } from '@memberjunction/generic-database-provider';
// 1. Create the Redis provider
const redisProvider = new RedisLocalStorageProvider({
url: 'redis://localhost:6379',
defaultTTLSeconds: 300, // 5-minute default TTL
});
// 2. Inject it into the data provider
const provider = Metadata.Provider as GenericDatabaseProvider;
provider.SetLocalStorageProvider(redisProvider);
// That's it! All MJ caching now flows through Redis.
The RedisProviderConfig object supports the following options:
| Option | Type | Default | Description |
|---|---|---|---|
url |
string |
— | Redis connection URL (redis:// or rediss:// for TLS). Mutually exclusive with options. |
options |
RedisOptions |
— | Full ioredis options object. Mutually exclusive with url. |
keyPrefix |
string |
'mj' |
Prefix for all Redis keys. Useful for isolating MJ data in a shared Redis instance. |
defaultTTLSeconds |
number |
undefined |
Default time-to-live for all cached entries. undefined means keys persist until explicitly removed. |
maxRetries |
number |
10 |
Maximum reconnection attempts with exponential backoff before giving up. |
enableLogging |
boolean |
true |
Whether to log connection events via MJ's LogStatus/LogError. |
# Start a local Redis container
docker run -d --name mj-redis -p 6379:6379 redis:7-alpine
const provider = new RedisLocalStorageProvider({
url: 'redis://localhost:6379',
defaultTTLSeconds: 300,
});
const provider = new RedisLocalStorageProvider({
url: `rediss://default:${process.env.AZURE_REDIS_KEY}@${process.env.AZURE_REDIS_HOST}:6380`,
defaultTTLSeconds: 600,
});
Or using the options object for more control:
const provider = new RedisLocalStorageProvider({
options: {
host: process.env.AZURE_REDIS_HOST,
port: 6380,
password: process.env.AZURE_REDIS_KEY,
tls: {}, // Required for Azure
db: 0,
},
defaultTTLSeconds: 600,
});
const provider = new RedisLocalStorageProvider({
options: {
host: 'my-cluster.abc123.use1.cache.amazonaws.com',
port: 6379,
tls: {}, // Required for encryption in transit
},
defaultTTLSeconds: 600,
});
const provider = new RedisLocalStorageProvider({
url: process.env.REDIS_URL, // Provided by the service
defaultTTLSeconds: 600,
});
┌─────────────────────────────────────────────────────────────┐
│ Application Layer │
│ (MJAPI, Angular, React, Custom Apps) │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌──────────────────────┐
│ LocalCacheManager │ Singleton — LRU eviction, TTL,
│ (MJCore) │ stats, category-based isolation
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ ILocalStorageProvider│ Abstract interface (MJCore)
└──────────┬───────────┘
│
┌────────────┼────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────┐ ┌────────────────┐
│ InMemory │ │ Browser │ │ Redis │
│ (default) │ │ (IDB/LS) │ │ (this package) │
└──────────────┘ └──────────┘ └────────┬───────┘
│
▼
┌─────────────┐
│ Redis Server │
│ (any host) │
└─────────────┘
All keys follow the pattern: {prefix}:{category}:{key}
"mj"), isolates MJ data in shared Redis instancesRunViewCache, Metadata, DatasetCache, RunQueryCache, defaultExample Redis keys:
mj:RunViewCache:Users|Active=1|Name ASC
mj:Metadata:___MJCore_Metadata_AllMetadata
mj:DatasetCache:MyDataset_items
mj:default:some-arbitrary-key
Each category has an associated Redis Set at {prefix}:__categories__:{category} that tracks all member keys. This enables efficient:
ClearCategory() — Deletes all keys in a category in a single pipelineGetCategoryKeys() — Lists all keys without scanning the entire keyspaceRedis has native key expiration, so TTL is handled efficiently at the server level:
defaultTTLSeconds applies to every SetItem() callSetItem(key, value, category, ttlSeconds) overrides the defaultClearCategory() is calledAll Redis operations are wrapped in try/catch. On failure:
null (cache miss, falls through to database)LogError() but don't crash the appioredis with configurable exponential backoffThis design ensures a Redis outage degrades performance (more database hits) but never causes application downtime.
RedisLocalStorageProvidernew RedisLocalStorageProvider(config?: RedisProviderConfig)
Creates a new provider and establishes a Redis connection. The connection is lazy — it happens on the first command, so construction itself does not block.
The interface is generic-typed — T flows from caller through to retrieved value:
| Method | Description |
|---|---|
GetItem<T>(key, category?) |
Retrieves a cached value. JSON-deserializes internally — returns the typed object. Returns null on miss, corrupt entry, or Redis unavailability. |
GetItems<T>(keys, category?) |
Batched read via Redis MGET — one command, one network round-trip, N values. Returns Map<string, T | null>. Missing/corrupt entries map to null per-key without failing the batch. ~N× faster than individual GetItem calls which each pay full RTT. |
SetItem<T>(key, value, category?, ttlSeconds?) |
Stores a value with optional TTL. JSON-serializes internally — pass plain objects/arrays/primitives. Uses pipeline for atomic set + category tracking. |
Remove(key, category?) |
Deletes a key and removes it from category tracking. |
ClearCategory(category) |
Deletes all keys in a category using the tracking Set. |
GetCategoryKeys(category) |
Returns all key names in a category. |
Internal serialization: Redis stores strings, so SetItem calls JSON.stringify(value) and GetItem calls JSON.parse(raw) automatically. Callers see a typed object interface — no manual JSON.parse/JSON.stringify needed.
Type fidelity caveats (JSON limitations apply):
Date instances become ISO strings on round-trip (caller must re-wrap with new Date(value) if a Date is needed)Map/Set become plain objects/arraysSetItem call resolves without throwing, but the value is not storedFor a richer storage model that preserves Date/Map/Set/typed arrays natively, use BrowserIndexedDBStorageProvider (browser-side) which leverages IndexedDB's structured clone algorithm.
interface CachedSession { userId: string; expiresAt: string; permissions: string[]; }
await provider.SetItem<CachedSession>('session:abc', {
userId: 'u-1',
expiresAt: '2026-06-01T00:00:00Z',
permissions: ['read', 'write']
}, 'Sessions', 3600); // 1-hour TTL
const session = await provider.GetItem<CachedSession>('session:abc', 'Sessions');
// ^^^^^^^ typed as CachedSession | null
if (session) {
console.log(session.permissions); // already typed
}
| Method | Description |
|---|---|
Exists(key, category?) |
Checks key existence without transferring the value (more efficient than GetItem). |
GetTTL(key, category?) |
Returns remaining TTL in seconds (-1 = no expiry, -2 = key doesn't exist). |
Ping() |
Health check — returns true if Redis responds with PONG. |
Disconnect() |
Graceful shutdown — sends QUIT and waits for pending replies. |
| Property | Type | Description |
|---|---|---|
IsConnected |
boolean |
Whether the client has an active connection (transient — auto-reconnects). |
Client |
Redis |
The underlying ioredis client for advanced operations (pub/sub, streams, etc.). |
The package includes comprehensive unit tests with mocked Redis:
cd packages/RedisProvider
npm run test
For end-to-end testing with a real Redis instance:
# Start Redis
docker run -d --name mj-redis -p 6379:6379 redis:7-alpine
# Verify connectivity
docker exec mj-redis redis-cli ping
# → PONG
# Run your MJAPI with Redis configured
REDIS_URL=redis://localhost:6379 npm run start:api
# Monitor Redis activity in real time
docker exec mj-redis redis-cli monitor
# Clean up
docker stop mj-redis && docker rm mj-redis
# See all MJ keys
docker exec mj-redis redis-cli KEYS "mj:*"
# Check memory usage
docker exec mj-redis redis-cli INFO memory
# See cache hit/miss stats
docker exec mj-redis redis-cli INFO stats | grep keyspace
| Category | Recommended TTL | Rationale |
|---|---|---|
Metadata |
30–60 minutes | Entity schema changes infrequently |
RunViewCache |
2–5 minutes | Balance freshness vs. database load |
RunQueryCache |
2–5 minutes | Same as RunViewCache |
DatasetCache |
5–10 minutes | Datasets are typically larger, change less often |
maxmemory and maxmemory-policy allkeys-lru in your Redis configuration so Redis automatically evicts least-recently-used keys when memory is fullINFO memory and set alerts on used_memory_peakdefaultTTLSeconds to ensure keys don't accumulate indefinitelyrediss:// URL scheme or tls: {} in options) for cloud-hosted Redisioredis — Feature-rich Redis client for Node.js@memberjunction/core — ILocalStorageProvider interface and logging utilities@memberjunction/global — Global object store utilities@memberjunction/core — Defines ILocalStorageProvider, LocalCacheManager, and InMemoryLocalStorageProvider@memberjunction/generic-database-provider — Where LocalStorageProvider is wired into the data provider chain@memberjunction/sqlserver-dataprovider — SQL Server provider (inherits caching from GenericDatabaseProvider)@memberjunction/postgresql-dataprovider — PostgreSQL provider (inherits caching from GenericDatabaseProvider)
Example