BentoCache Documentation

website·Indexed 32 days ago

https://bentocache.dev/docs/introduction

A robust multi-tier caching library for Node.js applications. Features include stampede protection, grace periods, adaptive caching, tagging, and support for multiple cache drivers such as Redis and Filesystem. It provides official integration for AdonisJS via the @adonisjs/cache package and includes plugins for Prometheus and OpenTelemetry.

Tokens
13.1K
Snippets
70
Records
87
Agent score
66%

What's inside BentoCache

  1. Overview of Bentocache multi-tier caching

    Bentocache is a full-featured caching library for Node.js designed for performance and flexibility. It supports two primary modes of operation:

    1. One-level mode: A standard caching method using a single driver (e.g., Redis, In-Memory, Filesystem, DynamoDB).
    2. Two-levels mode: A high-performance setup combining a local L1 cache (In-memory LRU) and a distributed L2 cache (e.g., Redis). In multi-instance environments, local caches can be synchronized via a Bus (Redis or RabbitMQ) to maintain integrity across instances.
  2. Prevent cache stampedes in Bentocache

    Bentocache includes a built-in mechanism to prevent cache stampedes. When multiple concurrent requests attempt to fetch the same expired key, Bentocache ensures only one request triggers the factory function to refresh the cache, while other concurrent requests wait for the result. This protection is transparent and requires no additional configuration.
  3. Synchronize multi-tier caches across instances using a Bus

    In a multi-instance deployment, a Bus is required to prevent 'stale' data in L1 (in-memory) caches. Without a bus, if Instance A invalidates a key in the distributed L2 cache, Instance B may still serve the old value from its own L1 cache. The bus (e.g., redisBusDriver) uses a messaging system (like Redis Pub/Sub) to notify all instances to invalidate specific keys. To optimize bandwidth and memory, the bus sends invalidation notifications rather than the actual data values, and uses a custom binary format instead of JSON.
  4. Install and configure the Prometheus plugin for Bentocache

    Bentocache provides a built-in plugin to expose metrics to Prometheus. To use it, install the prom-client and @bentocache/plugin-prometheus packages, then add the prometheusPlugin() to your BentoCache configuration.
    pnpm i prom-client @bentocache/plugin-prometheus
    import { BentoCache, bentostore } from 'bentocache'
    import { prometheusPlugin } from '@bentocache/plugin-prometheus'
    
    const bento = new BentoCache({
      default: 'multitier',
      plugins: [prometheusPlugin()],
      stores: { myCache: bentostore().useL1Layer(memoryDriver()) },
    })
    
  5. Implement grace periods to serve stale data during source downtime

    Grace periods allow BentoCache to serve stale data after the TTL (time to live) has expired if the factory function fails (e.g., due to a database or API outage). This prevents the application from returning an error to the user by providing slightly out-of-date data until the source of truth is restored.

    Configuration can be applied at the global, driver, or operation levels using the grace option.

    bento.getOrSet({
      key: 'users:1',
      factory: () => User.find(1),
      ttl: '10m',   // Data is fresh for 10 minutes
      grace: '6h',  // If stale, serve for up to 6 hours if factory fails
    })
  6. Implement Adaptive Caching to dynamically set TTL

    Adaptive caching allows you to change cache options (such as Time-To-Live) dynamically based on the data being cached. This is achieved by using the options argument provided to the factory function in getOrSet. By calling options.setOptions(), you can override the default cache configuration based on the properties of the fetched value, ensuring that data is cached for the optimal duration (e.g., matching an API token's actual expiration time or adjusting TTL based on the age of a content item).
    // Example 1: Setting TTL based on a token's expiration
    const authToken = await bento.getOrSet({
      key: 'token',
      factory: async (options) => {
        const token = await fetchAccessToken();
        // Dynamically set TTL based on the token's own expiration value
        options.setOptions({ ttl: token.expiresIn });
        return token;
      }
    });
    
    // Example 2: Setting TTL based on data volatility (e.g., news articles)
    const namespace = bento.namespace('news');
    const news = await namespace.getOrSet({
      key: newsId,
      factory: async (options) => {
        const newsItem = await fetchNews(newsId);
    
        if (newsItem.hasBeenUpdatedRecently) {
          options.setOptions({ ttl: '5m' }); // Short cache for volatile data
        } else {
          options.setOptions({ ttl: '2d' }); // Long cache for stable data
        }
    
        return newsItem;
      }
    });
  7. Implement a multi-tier cache with L1 and L2 layers

    A multi-tier caching system improves performance by using a fast in-memory cache (L1) as the first level and a larger distributed cache (L2) as the second level. Bentocache implements this by chaining .useL1Layer() and .useL2Layer() during store configuration. If the application runs on a single instance, only L1 and L2 are required; for multiple instances, a bus is needed to synchronize L1 caches.
    import { BentoCache, bentostore } from 'bentocache'
    import { memoryDriver } from 'bentocache/drivers/memory'
    import { redisDriver, redisBusDriver } from 'bentocache/drivers/redis'
    
    const redisConnection = { host: 'localhost', port: 6379 }
    const bento = new BentoCache({
      default: 'multitier',
      stores: {
        multitier: bentostore()
          // L1 Cache: In-memory with 10Mb limit (LRU eviction)
          .useL1Layer(memoryDriver({ maxSize: '10mb' }))
          // L2 Cache: Distributed Redis cache
          .useL2Layer(redisDriver({ connection: redisConnection }))
          // Bus: Synchronizes L1 caches across instances via Redis Pub/Sub
          .useBus(redisBusDriver({ connection: redisConnection })),
      }
    })
    
  8. Install and set up Bentocache OpenTelemetry instrumentation

    Bentocache provides an official OpenTelemetry instrumentation package, @bentocache/otel, which listens to Bentocache tracing channels and emits spans for operations such as cache.get, cache.getOrSet, cache.set, cache.delete, cache.deleteMany, cache.clear, cache.expire, and cache.factory.
    import { NodeSDK } from '@opentelemetry/sdk-node'
    import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
    import { BentoCacheInstrumentation } from '@bentocache/otel'
    
    const traceExporter = new OTLPTraceExporter({
      url: 'http://localhost:4318/v1/traces',
    })
    
    const sdk = new NodeSDK({
      serviceName: 'my-app',
      traceExporter,
      instrumentations: [
        new BentoCacheInstrumentation({
          requireParentSpan: true,
          includeKeys: false,
          suppressInternalOperations: true,
        }),
      ],
    })
    
    sdk.start()
  9. Use Orchid ORM as a BentoCache L2 layer

    To use the Orchid driver, provide an Orchid ORM instance. This driver specifically supports PostgreSQL. You must install the orchid-orm package to use this driver.
    import { createDb } from 'orchid-orm'
    import { BentoCache, bentostore } from 'bentocache'
    import { orchidDriver } from 'bentocache/drivers/orchid'
    
    export const db = createDb({ databaseURL: `postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}` })
    
    export const bento = new BentoCache({
      default: 'cache',
      stores: {
        cache: bentostore().useL2Layer(orchidDriver({ connection: db }))
      }
    })
    
  10. Configure Redis cache driver

    The Redis driver uses ioredis under the hood and supports providers like Upstash, Vercel KV, Valkey, KeyDB, and DragonFly. You must install ioredis to use this driver. You can provide connection options or pass an existing ioredis instance to reuse a connection.
    import { BentoCache, bentostore } from 'bentocache'
    import { redisDriver } from 'bentocache/drivers/redis'
    
    // Option 1: Using connection options
    const bento = new BentoCache({
      default: 'redis',
      stores: {
        redis: bentostore().useL2Layer(redisDriver({
          connection: { host: '127.0.0.1', port: 6379 }
        }))
      }
    })
    
    // Option 2: Using an existing ioredis instance
    import { Redis } from 'ioredis'
    const ioredis = new Redis()
    const bentoWithInstance = new BentoCache({
      default: 'redis',
      stores: {
        redis: bentostore().useL2Layer(redisDriver({
          connection: ioredis
        }))
      }
    })
    
  11. Implement a custom L2 cache driver

    To create a custom L2 cache driver, implement the L2CacheDriver interface from bentocache/types. The driver class constructor will receive ttl and prefix parameters. Once the driver is defined, you must create a factory function returning a CreateDriverResult to allow BentoCache to instantiate the driver at runtime.
    import type { L2CacheDriver, CreateDriverResult } from 'bentocache/types'
    import { BentoCache, bentostore } from 'bentocache'
    
    class MyDriver implements L2CacheDriver {
      async namespace(namespace: string) { /* ... */ }
      async get(key: string) { /* ... */ }
      async pull(key: string) { /* ... */ }
      async set(key: string, value: string, ttl?: number) { /* ... */ }
      async clear() { /* ... */ }
      async delete(key: string) { /* ... */ }
      async deleteMany(keys: string[]) { /* ... */ }
      async disconnect() { /* ... */ }
    }
    
    export function myDriver(options: MyDriverOptions): CreateDriverResult<MyDriver> {
      return {
        options,
        factory: (config: MyDriverOptions) => new MyDriver(config)
      }
    }
    
    // Usage
    const bento = new BentoCache({
      default: 'myStore',
      stores: {
        myStore: bentostore()
          .useL2Layer(myDriver({ /* Your driver options */ }))
      }
    })
    
  12. Create and register a BentoCache plugin

    BentoCache plugins are functions that return an object containing a register method. The register method is called internally by BentoCache during application startup and provides access to the BentoCache instance, allowing you to hook into events (such as cache:miss and cache:hit) or extend functionality. Plugins are enabled by passing them into the plugins array during the initialization of the BentoCache instance.
    import { BentoCache } from 'bentocache'
    import type { BentoCachePlugin } from 'bentocache/types'
    
    // 1. Define the plugin
    export function myBentoCachePlugin(): BentoCachePlugin {
      return {
         register(bentocache) {
            // Access the bentocache instance to listen to events
            bentocache.on('cache:miss', () => console.log('Cache miss!'))
            bentocache.on('cache:hit', () => console.log('Cache hit!'))
         }
       }
    }
    
    // 2. Register the plugin during instantiation
    const bentocache = new BentoCache({
      plugins: [myBentoCachePlugin()]
    })