Skip to main content
Version: Next

Caching in CommandKit

Caching is a technique that stores frequently accessed data in a temporary storage to improve performance and reduce load on your primary data sources. In Discord bots, this can significantly reduce database queries and external API calls.

CommandKit provides a powerful yet simple caching system through @commandkit/cache plugin. This guide will show you how to effectively use caching in your bot.

info

CommandKit makes the use of a custom "use cache" directive to simplify the caching boilerplate. This enables you to focus on your problem domain rather than unnecessary boilerplate logic.

Initializing the Cache Plugin

To get started with caching in CommandKit, you need to install the @commandkit/cache package:

npm install @commandkit/cache

Then, import the cache plugin and add it to your CommandKit configuration:

import { defineConfig } from 'commandkit';
import { cache } from '@commandkit/cache';

export default defineConfig({
plugins: [cache()],
});

This will set up the default in-memory cache provider. You can also configure a custom cache provider if needed, which we will discuss later in this guide.

Now any function that uses the "use cache" directive will automatically be cached.

async function fetchData() {
'use cache';

// This result will be cached
return database.getData();
}

Understanding CommandKit's Cache System

CommandKit's caching system is built on a provider-based architecture, allowing you to choose where your cache data is stored. The system includes:

  • In-memory caching (default)
  • Redis caching (via @commandkit/redis)
  • Custom cache providers (extendable)

Default In-Memory Cache

By default, CommandKit uses an in-memory cache provider. This means:

  • Cache data is stored in your bot's memory
  • Data is lost when the bot restarts
  • Perfect for development and small bots
  • No additional setup required

Setting Up Redis Cache

For production bots, Redis caching is recommended as it:

  • Persists across bot restarts
  • Can be shared across multiple bot instances
  • Provides better performance for large datasets

To use Redis caching:

import { defineConfig } from 'commandkit';
import { cache } from '@commandkit/cache';
import { redis } from '@commandkit/redis';

export default defineConfig({
plugins: [cache(), redis()],
});

For custom Redis configuration:

import { setCacheProvider } from '@commandkit/cache';
import { RedisCache } from '@commandkit/redis';
import { Redis } from 'ioredis';

const redis = new Redis({
// Your Redis configuration
host: 'localhost',
port: 6379,
});

setCacheProvider(new RedisCache(redis));

Using the Cache

CommandKit provides a simple and powerful way to implement caching using the "use cache" directive:

async function getUserProfile(userId: string) {
'use cache';

// This result will be cached
return await database.getUser(userId);
}

You can customize the cache behavior using helper functions:

import { cacheTag, cacheLife } from '@commandkit/cache';

async function getUserProfile(userId: string) {
'use cache';

// Set a custom cache key
cacheTag(`user:${userId}`);

// Set cache duration to 1 hour
cacheLife('1h');

return await database.getUser(userId);
}

Cache Duration Format

You can specify cache duration in multiple formats:

  • Milliseconds: ttl: 60000 (1 minute)
  • String shortcuts:
    • '5s' - 5 seconds
    • '1m' - 1 minute
    • '2h' - 2 hours
    • '1d' - 1 day

Managing Cache Data

Invalidating Cache

To remove cached data:

import { invalidate } from '@commandkit/cache';

// Remove specific cache entry
await invalidate('user:123');

Revalidating Cache

To force refresh cached data:

import { revalidate } from '@commandkit/cache';

// Revalidate and get fresh data
const freshData = await revalidate('user:123');

Real World Example: XP System

Let's build a simple XP system using CommandKit's caching:

import { ChatInputCommand, CommandData } from 'commandkit';
import { cacheTag } from '@commandkit/cache';
import { database } from '../database';

export const command: CommandData = {
name: 'xp',
description: 'Check your XP',
};

// Cached function to get user XP
async function getUserXP(guildId: string, userId: string) {
'use cache';

const key = `xp:${guildId}:${userId}`;
cacheTag(key);

const xp = (await database.get(key)) ?? 0;
return xp;
}

export const chatInput: ChatInputCommand = async ({ interaction }) => {
await interaction.deferReply();

const xp = await getUserXP(interaction.guildId!, interaction.user.id);

return interaction.editReply(`You have ${xp} XP!`);
};

XP Event Handler

import { invalidate } from '@commandkit/cache';
import { database } from '../database';

export default async function (message) {
if (message.author.bot || !message.inGuild()) return;

const key = `xp:${message.guildId}:${message.author.id}`;
const oldXp = (await database.get(key)) ?? 0;
const xp = Math.floor(Math.random() * 10) + 1;

await database.set(key, oldXp + xp);
await invalidate(key); // Invalidate cache after update
}

Best Practices

  1. Choose Appropriate Cache Duration

    • Short TTL for frequently changing data
    • Longer TTL for static content
    • Consider your data update patterns
  2. Use Meaningful Cache Keys

    • Include relevant IDs (e.g., user:123)
    • Use consistent naming patterns
    • Consider data relationships
  3. Handle Cache Misses

    • Always provide fallback values
    • Consider error cases
    • Implement proper error handling
  4. Invalidate Strategically

    • Invalidate when data changes
    • Use revalidate() for controlled updates
    • Consider cache dependencies
  5. Monitor Cache Performance

    • Watch memory usage with in-memory cache
    • Monitor Redis memory if using Redis
    • Adjust TTL based on usage patterns

Advanced Usage

Custom Cache Provider

You can create a custom cache provider by extending the CacheProvider class:

import { CacheProvider } from '@commandkit/cache';

class MyCustomCache extends CacheProvider {
async get<T>(key: string) {
// Implement get logic
}

async set<T>(key: string, value: T, ttl?: number) {
// Implement set logic
}

async exists(key: string) {
// Implement exists logic
}

async delete(key: string) {
// Implement delete logic
}

async clear() {
// Implement clear logic
}

async expire(key: string, ttl: number) {
// Implement expire logic
}
}

Then use it in your bot:

import { setCacheProvider } from '@commandkit/cache';

setCacheProvider(new MyCustomCache());

How It Works Under the Hood

The "use cache" directive is a powerful feature that transforms your functions into cached versions. Here's how it works:

  1. Compilation Process

    • The "use cache" directive is transformed into a higher-order function during compilation
    • This transformation wraps your original function with caching logic
    • The compiler handles all the boilerplate code for you
  2. Runtime Behavior

    • When a cached function is called, it first checks the cache provider for existing data
    • If cached data exists and is valid, it's returned immediately
    • If no cache exists or it's expired:
      • The original function is executed
      • The result is stored in the cache
      • The result is returned to the caller
  3. Cache Context

    • Each cached function call runs in a cache context
    • This context manages:
      • Cache keys and tags
      • TTL (Time To Live) settings
      • Cache invalidation rules
  4. Cache Provider Integration

    • The cache provider (in-memory or Redis) handles the actual storage
    • It manages:
      • Data serialization/deserialization
      • TTL enforcement
      • Memory management
      • Cache eviction policies

Here's a simplified view of what happens when you use the directive:

// Your code
async function getUserProfile(userId: string) {
'use cache';
cacheTag(`user:${userId}`);
return database.getUser(userId);
}

// What happens under the hood (simplified)
const cachedGetUserProfile = async (userId: string) => {
const cacheKey = `user:${userId}`;
const cached = await cacheProvider.get(cacheKey);

if (cached) return cached.value;

const result = await database.getUser(userId);
await cacheProvider.set(cacheKey, result, DEFAULT_TTL);
return result;
};

This architecture ensures:

  • Efficient caching with minimal boilerplate
  • Consistent cache behavior across your application
  • Easy integration with different cache providers
  • Type safety and proper error handling