Skip to main content
Version: Next

Caching

Caching is a technique used to store data in a temporary storage to reduce the time it takes to fetch the data from the original source. This can be useful in Discord bots to reduce the number of database queries or external API calls.

CommandKit provides an easy way to implement caching in your bot without having to worry about the underlying implementation. This guide will show you how to use the caching feature in CommandKit.

Setting up the cache

By default, commandkit enables in-memory caching. This means that the cache will be stored in the bot's memory and will be lost when the bot restarts. You can provide a custom cache store by specifying the cacheProvider option when instantiating CommandKit.

import { CommandKit } from 'commandkit';

new CommandKit({
client,
commandsPath,
eventsPath,
cacheProvider: new MyCustomCacheProvider(),
});

The MyCustomCacheProvider class should extend CacheProvider from CommandKit and implement the required methods. You may use this to store the cache in redis, a database or a file system.

CommandKit officially provides a RedisCache provider as well with @commandkit/redis package. You can use it as follows:

import { CommandKit } from 'commandkit';
import { RedisCache } from '@commandkit/redis';

new CommandKit({
client,
commandsPath,
eventsPath,
// uses default redis connection options (ioredis package)
cacheProvider: new RedisCache(),
});

Real World Example: XP System

Let's build a simple XP system using CommandKit's caching feature. We'll create:

  • A message event to give XP
  • A command to check XP
  • Commands to manage the XP cache

XP Command

import { SlashCommandProps, CommandData, cacheTag } from 'commandkit';
import { database } from '../database';

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

// This function will be cached
async function getUserXP(guildId, userId) {
'use cache';

const key = `xp:${guildId}:${userId}`;
cacheTag(key); // Use the database key as cache tag

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

export async function run({ 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';
import { database } from '../database';

// Give XP when user sends a message
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 so next xp check is fresh
}

In this example:

  1. The XP command caches user XP data to avoid database queries
  2. When a user sends a message, they get random XP
  3. The cache is invalidated after XP update so next check shows new value

Common Caching Patterns

Pattern 1: Cache with Auto-Expiry

import { cache } from 'commandkit';

// Cache API results for 5 minutes
const fetchPokemon = cache(
async (name) => {
const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`);
return response.json();
},
{
tag: 'pokemon-data',
ttl: '5m', // Expires after 5 minutes
},
);

Pattern 2: Manual Cache Control

// Using "use cache" directive with manual control
async function fetchUserProfile(userId) {
'use cache';

cacheTag({
tag: `user-${userId}`,
ttl: '1h',
});

return database.getUserProfile(userId);
}

// Invalidate cache when profile updates
async function updateUserProfile(userId, data) {
await database.updateUserProfile(userId, data);
await invalidate(`user-${userId}`);
}

Pattern 3: Forced Refresh with Revalidation

import { revalidate as revalidate } from 'commandkit';

// Command to force refresh server stats
export async function run({ interaction }) {
await interaction.deferReply();

// Force refresh and get new data
const newStats = await revalidate('server-stats');

return interaction.editReply({
content: `Stats refreshed! New member count: ${newStats.members}`,
});
}

Cache Duration Format

You can specify cache duration in multiple formats:

  • As milliseconds: ttl: 60000 (1 minute)
  • As string shortcuts:
    • '5s' - 5 seconds
    • '1m' - 1 minute
    • '2h' - 2 hours
    • '1d' - 1 day etc.

Best Practices

  1. Choose Good Cache Keys:

    • Use meaningful, unique cache tags
    • Include relevant IDs (like user-123 or guild-456)
  2. Set Appropriate TTL:

    • Short TTL for frequently changing data
    • Longer TTL for static content
    • Consider invalidating manually for important updates
  3. Handle Cache Misses:

    • Cache functions should handle cases when data isn't found
    • Provide default values when appropriate
  4. Invalidate Strategically:

    • Invalidate cache when underlying data changes
    • Consider using revalidate() for controlled updates

Using the cache

Using commandkit CLI

async function fetchData() {
'use cache';

// Fetch data from an external source
const data = await fetch('https://my-example-api.com/data');

return data.json();
}
tip

The 'use cache' directive will only work if you are using the commandkit cli to run your bot. Any function decorated with 'use cache' will be automatically cached.

Using the cache manually

import { cache } from 'commandkit';

const fetchData = cache(
async () => {
// Fetch data from an external source
const data = await fetch('https://my-example-api.com/data');
return data.json();
},
{
tag: 'user-data', // Optional cache tag name
ttl: '1m', // TTL as string or number (in ms)
},
);

By default, the cached data will be stored for 15 minutes unless revalidate() or invalidate() is called. You can specify a custom TTL (time to live) either as milliseconds or as a time string.

Setting Cache Parameters

When using the "use cache" directive, you can use cacheTag() to set cache parameters:

import { cacheTag } from 'commandkit';

async function fetchData() {
'use cache';

cacheTag({
tag: 'user-data', // cache tag name
ttl: '1m', // TTL as string or number (in ms)
});
// Or just set the tag name
cacheTag('user-data');

const data = await fetch('https://my-example-api.com/data');
return data.json();
}

You can also set just the TTL using cacheLife:

import { cacheLife } from 'commandkit';

async function fetchData() {
'use cache';

cacheLife('1m'); // TTL as string
// or
cacheLife(60_000); // TTL in milliseconds

const data = await fetch('https://my-example-api.com/data');
return data.json();
}
tip

cacheTag() will only tag the function when it first runs. If not tagged manually, CommandKit assigns a random tag name with 15 minutes TTL. cacheTag() must be used with the cache() function or a function with "use cache" directive only.

Managing Cache Data

Invalidating Cache

To immediately remove cached data:

import { invalidate } from 'commandkit';

// Remove the cache entry immediately
await invalidate('user-data');

Revalidating Cache

To force refresh the cached data:

import { revalidate } from 'commandkit';

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

The revalidation process will:

  1. Remove the existing cache entry
  2. Re-execute the cached function to get fresh data
  3. Store the new data in cache
  4. Return the fresh data