Skip to content
V2 has arrived! A lot has changed, so treat this as a fresh start.

Caching

Honocord uses a modular caching system that allows you to easily integrate different caching backends, such as in-memory, Durable Objects, MongoDB, or your own custom implementation. This guide will walk you through the basics of Honocord’s caching system, how to set it up, and best practices for using it in your implementation.

Honocord provides several built-in cache adapters:

You can also create your own custom cache adapter by implementing the abstract BaseCacheAdapter class.

It is important to note that you cannot use every adapter in every environment.

AdapterCloudflare WorkersNode.js / Bun
MemoryCacheAdapter
DurableObjectCacheAdapter
MongoCacheAdapter *?

* I’m not really sure about this one, it could work on CF Workers, but I haven’t tested it yet. If you try it out, let me know how it goes!

Every caching setup is basically the same, as you have to use the withCache method on the Honocord instance to provide a factory function that creates your cache adapter.
Only the setup before that may vary slightly based on the adapter.

Terminal window
pnpm add @honocord/cache-memory
pnpm add @honocord/cache-do
pnpm add @honocord/cache-mongo
pnpm add @honocord/cache-base # for custom adapters
import { Honocord } from "honocord";
import { MemoryCacheAdapter } from "@honocord/cache-memory";
const cache = new MemoryCacheAdapter({
ttl: 300,
cleanupInterval: 600, // Interval in seconds for automatic cleanup of expired entries. This is only supported by this adapter as of now.
});
const bot = new Honocord().withCache(() => cache);

The cache polulates automatically every time an interaction is received.

However, some things are not automatically cached due to Discord only providing partial objects in certain cases. This includes:

  • Guilds - Discord only sends a partial guild object with id, features and the preferred locale; This doesn’t need to be cached.
  • Resolved Channels - When a user interacts with a message component or a modal, and it has a channel select menu where resolved channels are included in the interaction data, those channels are not automatically cached. These channels are only partial and don’t include any useful information other than the ID and type.

Cache Diagram

As you can see, you effectivly only have to interact with the CacheManager. The CacheManager is responsible for managing the different namespaces and providing a unified interface for accessing the cache.
When you call ctx.cache.getUser(userId), the CacheManager will delegate the call to the appropriate namespace accessor, which will then interact with the underlying cache adapter to retrieve the data.

If you write a custom cache adapter, you only need to extend the abstract BaseCacheAdapter class and implement the required methods.

import Redis from "iovalkey"; // works for both Valkey AND Redis
export class RedisCacheAdapter implements CacheAdapter {
private client: Redis;
private ready: Promise<void>;
constructor(urlOrOptions: string | Redis.RedisOptions) {
this.client = new Redis(urlOrOptions as any);
this.ready = new Promise((resolve, reject) => {
this.client.once("ready", resolve);
this.client.once("error", reject);
});
}
async connect(): Promise<this> {
await this.ready;
return this;
}
async get<T>(key: string): Promise<T | null> {
await this.ready;
const val = await this.client.get(key);
if (!val) return null;
return JSON.parse(val) as T;
}
async set<T>(key: string, value: T, ttlMs?: number): Promise<void> {
await this.ready;
const serialized = JSON.stringify(value);
if (ttlMs) {
await this.client.set(key, serialized, "PX", ttlMs); // PX = milliseconds TTL
} else {
await this.client.set(key, serialized);
}
}
async mset(entries: { key: string; value: unknown; ttlMs?: number }[]): Promise<void> {
await this.ready;
if (entries.length === 0) return;
const pipeline = this.client.pipeline();
for (const { key, value, ttlMs } of entries) {
const serialized = JSON.stringify(value);
if (ttlMs) {
pipeline.set(key, serialized, "PX", ttlMs);
} else {
pipeline.set(key, serialized);
}
}
await pipeline.exec();
}
async delete(key: string): Promise<void> {
await this.ready;
await this.client.del(key);
}
async clear(): Promise<void> {
await this.ready;
await this.client.flushdb();
}
}

Then use it with Honocord just like any built-in adapter:

import { Honocord } from "honocord";
import { RedisCacheAdapter } from "./RedisCacheAdapter";
const cache = new RedisCacheAdapter(process.env.REDIS_URL!, {
namespace: "my-bot",
ttl: 300, // 5-minute default TTL
});
await cache.connect(); // You can also don't do this and let Honocord handle connecting, but doing it yourself allows you to catch connection errors at startup
const bot = new Honocord().withCache(() => cache);

On every interaction, you have a property .fetcher which is an instance of Fetcher. The Fetcher is a utility class that provides methods for fetching data from the Discord API and automatically caching it, if not already cached.

For example, if you want to fetch a user by their ID, you can do:

const user = await ctx.fetcher.users.get(userId);

This will first check if the user is already cached, and if so, it will return the cached user. If not, it will fetch the user from the Discord API, cache it, and then return it.