lru-cache
This module is available to use in your EdgeWorkers functions to keep frequently used data in memory between events. The lru-cache module provides an in-memory, least-recently-used (LRU) cache with optional time-to-live (TTL) expiration, size-based eviction, and request coalescing.
A cache is scoped to the EdgeWorker instance that created it. Entries stored while handling one event are available to later events handled by the same instance.
The cache is not shared between EdgeWorkers instances or edge servers, and it is not persistent. Entries can disappear at any time, for example when the instance is restarted. Use the cache as an optimization only, and always be ready to load a value that is missing.
Entries are visible to every subsequent event handled by the same instance, regardless of which end user triggered it. Do not cache user-specific or sensitive data under keys that other requests can produce.
Cached values are held in the EdgeWorker's memory and count towards its memory limit. Use
max, and optionallymaxSize, orttlto bound how much the cache can grow.
LRUCache Object
There are three steps to using the lru-cache.
-
Create a new
LRUCacheat module scope.
Create the cache outside of your event handlers so that the same cache, and its entries, are reused across events. If you create a cache inside a handler it is discarded when the handler finishes. -
Store values with
set(), or configure afetchMethodand letfetch()store them for you. -
Read from the cache with
get()orfetch().
Consider this EdgeWorkers function that caches origin responses for one minute.
import { LRUCache } from 'lru-cache';
import { httpRequest } from 'http-request';
// (1) Create the cache once, at module scope.
const cache = new LRUCache({ max: 100, ttl: 60_000 });
export async function onClientRequest(request) {
// (2) Look the value up in the cache.
let body = cache.get(request.path);
if (body === undefined) {
const orginResponse = await httpRequest(`/origin/${request.path}`);
if (!orginResponse.ok) {
return request.respondWith(502, {}, `Origin returned ${orginResponse.status}`);
}
body = await orginResponse.text();
// (3) Store the value for subsequent events.
cache.set(request.path, body);
}
return request.respondWith(200, {}, body);
}The first request for a path loads the body from the origin. Later requests for the same path, handled by the same EdgeWorker instance within 60 seconds, are served from memory.
Use
fetch()with afetchMethodto combine the lookup and the load into a single call. Concurrent requests for the same key then share a single origin request.
Constructor
new LRUCache(options)Creates a new cache. All options other than max are optional.
import { LRUCache } from 'lru-cache';
const cache = new LRUCache({
max: 500,
ttl: 5 * 60_000,
updateAgeOnGet: true,
});| Option | Type | Description |
|---|---|---|
allowStale | Boolean | Return stale values from get(), peek(), and fetch(). Defaults to false. |
allowStaleOnFetchRejection | Boolean | Resolve fetch() with the stale value, or undefined, when fetchMethod throws or rejects. Defaults to false. |
backgroundFetchSize | Number | Size reserved for a pending fetch() that has no stale value. Only applies to size-constrained caches with a fetchMethod. Defaults to 1. |
fetchMethod | Function | Loads values for fetch() and forceFetch(). See fetchMethod. |
max | Number | Required. Maximum number of entries. Must be an integer greater than 0. When a new entry would exceed this limit, the least recently used entry is evicted. |
maxEntrySize | Number | Maximum size of a single entry. Defaults to maxSize. Cannot exceed maxSize. Larger entries are not stored. |
maxSize | Number | Maximum total size of all entries. Cannot exceed MAX_SIZE. Every entry needs a size, either from the size option of set() or from sizeCalculation. |
noDeleteOnFetchRejection | Boolean | Keep a stale entry in the cache when fetchMethod throws or rejects. Defaults to false. |
noDeleteOnStaleGet | Boolean | Do not delete stale entries when they are read by get(). Defaults to false. |
noUpdateTTL | Boolean | Keep the existing TTL when set() overwrites an entry. Defaults to false. |
sizeCalculation | Function | (value, key) => number. Computes the size of an entry. Must synchronously return a non-negative integer. Requires maxSize or maxEntrySize. |
ttl | Number | Default time-to-live of an entry, in milliseconds. Enables TTL tracking. Cannot exceed the maximum TTL. 0 means entries do not expire. Can be overrided by set or fetch methods. |
ttlAutopurge | Boolean | Automatically remove entries when their TTL expires. Defaults to false. |
ttlResolution | Number | Minimum time, in milliseconds, between autopurge checks. Only used with ttlAutopurge, optional. |
updateAgeOnGet | Boolean | Renew an entry's TTL when it is read by get() or returned by fetch(). Defaults to false. |
updateAgeOnHas | Boolean | Renew an entry's TTL when it is checked by has(). Defaults to false. |
The constructor throws a TypeError if options is not an object, if max is missing or 0, or if an option has the wrong type. It throws a RangeError if a size or TTL limit is exceeded. See Errors.
Limits
| Limit | Value | Description |
|---|---|---|
MAX_SIZE | Platform defined | Largest maxSize a cache can be created with. Exported by the module, so you can read it at runtime. Can vary between different tiers. |
Maximum ttl | 24 hours (86400000 ms) | Largest ttl parameter a cache can be created with. |
import { LRUCache, MAX_SIZE } from 'lru-cache';
const cache = new LRUCache({
max: 1000,
maxSize: Math.min(MAX_SIZE, 1024 * 1024),
sizeCalculation: (value) => value.length,
});Keys
Keys must be strings or numbers. Keys are converted to strings, so 1 and '1' refer to the same entry. Any other key type throws a TypeError.
Values
Any JavaScript value can be stored, except undefined. Calling set(key, undefined) deletes the entry.
Values are stored by reference. If you store an object and then modify it, later reads return the modified object.
Eviction
When adding an entry would exceed max or maxSize, the cache evicts the least recently used entries until the new entry fits.
get(), set(), and fetch() hits mark an entry as most recently used. has(), peek(), info(), and getRemainingTTL() do not change the order.
Time-to-live
Set a default ttl in the constructor, or a per-entry ttl in set() or fetch().
- Time is measured using the event time, which is the same value
Date.now()returns in the event. Note that time does not advance within a single event. - By default, an entry's TTL is measured from the time it was stored. Use the
startoption to measure it from a different point in time, in epoch milliseconds. - An entry becomes stale when its TTL has elapsed. Stale entries are not returned unless
allowStaleis set. get()deletes a stale entry when it reads it, unlessnoDeleteOnStaleGetis set. Other stale entries stay in the cache until they are read, evicted, removed bypurgeStale(), or removed byttlAutopurge.
const cache = new LRUCache({ max: 100, ttl: 60_000 });
cache.set('short', 'value', { ttl: 5_000 }); // Expires after 5 seconds.
cache.set('default', 'value'); // Expires after 60 seconds.
cache.set('forever', 'value', { ttl: 0 }); // Does not expire.Size Tracking
A cache is size-constrained when maxSize or maxEntrySize is set. Every entry in a size-constrained cache needs a size. The cache uses the size option passed to set() or fetch() if present, otherwise it calls sizeCalculation. If neither is available, set() throws a TypeError.
Entries larger than maxEntrySize are not stored. If the key already had an entry, that entry is removed. Pass a status object to detect this case through status.maxEntrySizeExceeded.
const cache = new LRUCache({
max: 1000,
maxSize: 64 * 1024,
maxEntrySize: 8 * 1024,
sizeCalculation: (value, key) => key.length + value.length,
});
cache.set('greeting', 'hello'); // Size computed by sizeCalculation.
cache.set('blob', someString, { size: 4096 }); // Size provided explicitly.Properties
All properties are read-only and reflect the options used to create the cache.
| Property | Type | Description |
|---|---|---|
allowStale | Boolean | The allowStale option. |
allowStaleOnFetchRejection | Boolean | The allowStaleOnFetchRejection option. |
backgroundFetchSize | Number | The backgroundFetchSize option. |
calculatedSize | Number | Total size of all entries. 0 when the cache is not size-constrained. |
fetchMethod | Function | The fetchMethod option, or undefined. |
max | Number | The max option. |
maxEntrySize | Number | The maxEntrySize option. 0 when the cache is not size-constrained. |
maxSize | Number | The maxSize option. 0 when not set. |
noDeleteOnFetchRejection | Boolean | The noDeleteOnFetchRejection option. |
noDeleteOnStaleGet | Boolean | The noDeleteOnStaleGet option. |
noUpdateTTL | Boolean | The noUpdateTTL option. |
size | Number | Number of entries currently in the cache. |
sizeCalculation | Function | The sizeCalculation option, or undefined. |
ttl | Number | The default ttl, in milliseconds. 0 when not set. |
updateAgeOnGet | Boolean | The updateAgeOnGet option. |
updateAgeOnHas | Boolean | The updateAgeOnHas option. |
Methods
clear()
Removes all entries from the cache.
cache.clear();delete()
Removes an entry. Returns true if an entry was removed, or false if the key was not in the cache.
If a fetch() is in flight for the key, its result is discarded and pending fetch() calls for the key reject with 'deleted'.
cache.set('a', 1);
cache.delete('a'); // true
cache.delete('a'); // false| Parameter | Type | Description |
|---|---|---|
| key | String | Number | The key to remove. |
fetch()
Returns a Promise that resolves with the cached value, or loads the value with the configured fetchMethod and stores it.
- If a fresh value is cached, the promise resolves with it and
fetchMethodis not called. - If no value is cached, or the value is stale,
fetchMethodis called and the promise resolves with its result. - If
fetch()is already in flight for the key, the call waits for that load instead of starting another one. - If no
fetchMethodis configured,fetch()behaves likeget().
import { LRUCache } from 'lru-cache';
import { httpRequest } from 'http-request';
const products = new LRUCache({
max: 500,
ttl: 5 * 60_000,
allowStale: true,
fetchMethod: async (key) => {
const fromOrigin = await httpRequest(`/api/products/${key}`);
if (!fromOrigin.ok) {
throw new Error(`Origin returned ${fromOrigin.status}`);
}
return await fromOrigin.json();
},
});
export async function onClientRequest(request) {
try {
const product = await products.fetch(id);
request.respondWith(200, { 'Content-Type': ['application/json'] }, JSON.stringify(product));
} catch (error) {
request.respondWith(502, {}, 'Product unavailable');
}
}| Parameter | Type | Description |
|---|---|---|
| key | String | Number | The key to look up or load. |
| options | Object | (Optional) Overrides for this call. See the table below. |
| Option | Type | Description |
|---|---|---|
allowStale | Boolean | If the entry is stale, resolve immediately with the stale value while fetchMethod refreshes it in the background. |
updateAgeOnGet | Boolean | Renew the entry's TTL on a fresh cache hit. |
noDeleteOnStaleGet | Boolean | Only used when no fetchMethod is configured and fetch() falls back to get(). |
ttl | Number | TTL of the stored result, in milliseconds. |
noUpdateTTL | Boolean | Keep the existing entry's TTL when storing the result. Ignored for new entries. |
size | Number | Size of the stored result. Skips sizeCalculation. |
allowStaleOnFetchRejection | Boolean | If fetchMethod fails, resolve with the stale value, or undefined, instead of rejecting. |
noDeleteOnFetchRejection | Boolean | If fetchMethod fails, keep the stale entry in the cache. |
forceRefresh | Boolean | Call fetchMethod even if a fresh value is cached. |
context | Any | Passed to fetchMethod as context. |
status | Object | Populated with details about this call. See Status Object. |
When
fetchMethodneeds to be called,fetch()must run inside an event handler. Calling it from module scope, or from a promise that outlives the event that created it, rejects with aTypeError. The event that starts a load stays active untilfetchMethodsettles, even if it returned a stale value immediately.
forceFetch()
Same as fetch(), but the returned promise rejects if the result is undefined. Use it when a missing value should be treated as an error.
When the result is undefined, the promise rejects with the string 'fetch() returned undefined'.
try {
const config = await cache.forceFetch('config');
// config is never undefined here.
} catch (error) {
// fetchMethod failed or returned undefined.
}forceFetch() takes the same parameters as fetch(). When a status object is passed, status.op is set to 'fetch'.
get()
Returns the value for a key, or undefined if the key is not in the cache. Marks the entry as most recently used.
If the entry is stale, get() returns undefined and deletes the entry. Set allowStale to return the stale value instead, and noDeleteOnStaleGet to keep the entry.
const cache = new LRUCache({ max: 10 });
cache.set('color', 'blue');
cache.get('color'); // 'blue'
cache.get('missing'); // undefined| Parameter | Type | Description |
|---|---|---|
| key | String | Number | The key to look up. |
| options | Object | (Optional) Overrides for this call. See the table below. |
| Option | Type | Description |
|---|---|---|
allowStale | Boolean | Return the value even if it is stale. |
updateAgeOnGet | Boolean | Renew the entry's TTL if it is not stale. |
noDeleteOnStaleGet | Boolean | Keep a stale entry in the cache after reading it. |
status | Object | Populated with details about this call. See Status Object. |
getRemainingTTL()
Returns the remaining TTL of an entry, in milliseconds.
| Result | Meaning |
|---|---|
0 | The key is not in the cache. |
Infinity | The entry has no TTL. |
| Positive | Milliseconds until the entry becomes stale. |
| Negative | The entry is stale. The value is how long ago it expired. |
const cache = new LRUCache({ max: 10, ttl: 60_000 });
cache.set('a', 1);
cache.getRemainingTTL('a'); // 60000
cache.getRemainingTTL('missing'); // 0| Parameter | Type | Description |
|---|---|---|
| key | String | Number | The key to inspect. |
has()
Returns true if a non-stale entry exists for the key. Does not change the recency of the entry.
const cache = new LRUCache({ max: 10 });
cache.set('a', 1);
cache.has('a'); // true
cache.has('b'); // false| Parameter | Type | Description |
|---|---|---|
| key | String | Number | The key to check. |
| options | Object | (Optional) Overrides for this call. See the table below. |
| Option | Type | Description |
|---|---|---|
updateAgeOnHas | Boolean | Renew the entry's TTL if it is present and not stale. |
status | Object | Populated with details about this call. See Status Object. |
info()
Returns details about an entry, or undefined if the key is not in the cache. Does not change the recency or TTL of the entry.
const cache = new LRUCache({ max: 10, ttl: 60_000 });
cache.set('a', 'value');
cache.info('a');
// { value: 'value', ttl: 60000, start: 1792490924343 }The returned object has the following properties.
| Property | Type | Description |
|---|---|---|
value | Any | The cached value. |
ttl | Number | Remaining TTL in milliseconds. Only present if the entry has a TTL. |
start | Number | Current event time in epoch milliseconds. Only present if ttl is present. |
size | Number | Size of the entry. Only present if the cache is size-constrained. |
Tip
The
ttl,start, andsizeproperties can be passed directly toset()to copy an entry into another cache with the same remaining lifetime.
| Parameter | Type | Description |
|---|---|---|
| key | String | Number | The key to inspect. |
peek()
Returns the value for a key without any side effects. peek() does not change the recency of the entry, renew its TTL, or delete it if it is stale.
If the entry is stale, peek() returns undefined unless allowStale is set.
cache.set('a', 1);
cache.set('b', 2);
cache.peek('a'); // 1, 'a' is still the least recently used entry.
cache.pop(); // 1| Parameter | Type | Description |
|---|---|---|
| key | String | Number | The key to look up. |
| options | Object | (Optional) Overrides for this call. See the table below. |
| Option | Type | Description |
|---|---|---|
allowStale | Boolean | Return the value even if it is stale. |
status | Object | Populated with details about this call. TTL fields are not set. See Status Object. |
pop()
Removes the least recently used entry and returns its value. Returns undefined if the cache is empty.
cache.set('a', 1);
cache.set('b', 2);
cache.pop(); // 1
cache.pop(); // 2
cache.pop(); // undefinedpurgeStale()
Removes all stale entries. Returns true if any entries were removed.
Use purgeStale() to reclaim memory from expired entries without waiting for them to be read or evicted. To do this automatically, set the ttlAutopurge option instead.
set()
Stores a value under a key and marks the entry as most recently used. If the key already exists, its value is replaced.
Passing undefined as the value deletes the entry.
const cache = new LRUCache({ max: 100, ttl: 60_000 });
cache.set('a', { id: 1 });
cache.set('b', 'short lived', { ttl: 1_000 });| Parameter | Type | Description |
|---|---|---|
| key | String | Number | The key to store the value under. |
| value | Any | The value to store. undefined deletes the entry. |
| options | Object | (Optional) Overrides for this call. See the table below. |
| Option | Type | Description |
|---|---|---|
ttl | Number | TTL of the entry, in milliseconds. 0 means the entry does not expire. |
start | Number | Epoch milliseconds from which ttl is measured. |
noUpdateTTL | Boolean | Keep the existing entry's TTL when overwriting it. Ignored for new entries. |
size | Number | Size of the entry. Skips sizeCalculation. |
status | Object | Populated with details about this call. See Status Object. |
If a fetch() is in flight for the key, the fetched value is discarded and pending fetch() calls for the key reject with 'replaced'.
fetchMethod
The fetchMethod option is a function that loads a value for fetch() and forceFetch(). It can return a value or a Promise. It is called with this set to undefined.
fetchMethod(key, staleValue, { options, context })| Parameter | Type | Description |
|---|---|---|
| key | String | Number | The key passed to fetch(). |
| staleValue | Any | The stale value currently cached, or undefined. |
| options | Object | The effective options for this fetch. Some of them can be modified to change how the result is stored. See the table below. |
| context | Any | The context option passed to fetch(), if any. |
| Property | Type | Writable | Description |
|---|---|---|---|
ttl | Number | Yes | TTL of the stored result. Only present if the cache tracks TTL. |
start | Number | Yes | Epoch milliseconds from which ttl is measured. |
noUpdateTTL | Boolean | Yes | Keep the existing entry's TTL when storing the result. |
size | Number | Yes | Size of the stored result. Only present if the cache is size-constrained. |
allowStaleOnFetchRejection | Boolean | Yes | Resolve fetch() with the stale value if this fetch fails. |
noDeleteOnFetchRejection | Boolean | Yes | Keep the stale entry in the cache if this fetch fails. |
allowStale | Boolean | No | Resolved allowStale for this call. |
updateAgeOnGet | Boolean | No | Resolved updateAgeOnGet for this call. |
noDeleteOnStaleGet | Boolean | No | Resolved noDeleteOnStaleGet for this call. |
forceRefresh | Boolean | No | Whether this fetch was forced. |
This example caches "not found" responses for a shorter time than successful ones.
import { httpRequest } from 'http-request';
import { LRUCache } from 'lru-cache';
const cache = new LRUCache({
max: 1000,
ttl: 10 * 60_000,
fetchMethod: async (key, staleValue, { options }) => {
const response = await httpRequest(`/api/items/${encodeURIComponent(key)}`);
if (response.status === 404) {
options.ttl = 30_000;
return null;
}
if (!response.ok) {
throw new Error(`Origin returned ${response.status}`);
}
return response.json();
},
});Concurrent Fetches
Calls to fetch() for the same key share a single fetchMethod call while it is in flight, including calls made by other events handled by the same instance. All callers receive the same result.
// fetchMethod is called once. Both promises resolve with the same value.
const [a, b] = await Promise.all([cache.fetch('key'), cache.fetch('key')]);Fetch Failures
When fetchMethod throws or returns a rejected promise:
- By default, the stale entry is deleted and
fetch()rejects with the error. - With
allowStaleOnFetchRejection,fetch()resolves with the stale value, orundefinedif there is none. - With
noDeleteOnFetchRejection, the stale entry is kept in the cache.
If the entry is deleted, replaced, or evicted while the fetch is in flight, the result is discarded and status.fetchAborted is set. Pending fetch() calls for the key reject with 'deleted', 'replaced', or 'evicted'.
Status Object
Most methods accept a status option. When you pass an object, the method populates it with details about the call. This is useful for logging and debugging.
import { logger } from 'log';
const getStatus = {};
const value = cache.get('config', { status: getStatus });
logger.log(`get ${status.key}: ${status.get}`); // For example, 'get config: hit'| Property | Type | Description |
|---|---|---|
op | String | The method that populated the status: 'get', 'has', 'set', 'peek', or 'fetch'. forceFetch() reports 'fetch'. |
key | String | The key, converted to a string. |
get | String | Result of get(): 'hit', 'stale', 'stale-fetching', 'fetching', or 'miss'. |
has | String | Result of has(): 'hit', 'stale', or 'miss'. |
peek | String | Result of peek(): 'hit', 'stale', or 'miss'. |
set | String | Result of set(): 'add', 'replace', or 'miss' if the entry was not stored. |
fetch | String | Result of fetch(): 'hit', 'miss', 'stale', 'refresh', 'inflight', or 'get' when no fetchMethod is configured. |
ttl | Number | TTL of the entry, in milliseconds. |
start | Number | Epoch milliseconds from which ttl is measured. |
now | Number | Current event time, in epoch milliseconds. |
remaining | Number | Remaining TTL, in milliseconds. |
returnedStale | Boolean | A stale value was returned. |
maxEntrySizeExceeded | Boolean | The entry exceeded maxEntrySize and was not stored. |
forceRefresh | Boolean | forceRefresh was requested. |
fetchDispatched | Boolean | fetchMethod was called. |
fetchResolved | Boolean | fetchMethod returned or resolved with a value. |
fetchRejected | Boolean | fetchMethod threw or returned a rejected promise. |
fetchAborted | Boolean | The entry was deleted, replaced, or evicted while the fetch was in flight, so the result was discarded. |
fetchUpdated | Boolean | The fetched value was stored in the cache. |
fetchError | Any | The error thrown by fetchMethod. |
Properties are only set when they apply to the call.
Errors
| Error | Description |
|---|---|
TypeError | options is not an object, max is missing or 0, or an option has the wrong type, or is out of range. |
TypeError | A key is missing, or is not a string or number. |
TypeError | set() was called on a size-constrained cache without a size option or sizeCalculation. |
TypeError | fetch() needed to call fetchMethod outside of an active event handler. The returned promise is rejected. |
TypeError | backgroundFetchSize is larger than maxEntrySize. The returned promise is rejected. |
RangeError | maxSize exceeds MAX_SIZE, maxEntrySize exceeds maxSize, ttl exceeds the maximum TTL, or sizeCalculation is set without maxSize or maxEntrySize. |
TypeError | sizeCalculation did not return an integer. |
RangeError | sizeCalculation returned a negative number. |
Updated about 1 hour ago
