Caching and memory
Athena caches Discord objects in memory using Collection (a typed Map). Defaults are fine for most bots. For large bots the cache can dominate memory;...
Athena caches Discord objects in memory using Collection (a typed Map). Defaults are fine for most bots. For large bots the cache can dominate memory; this page covers the knobs. Everything here is opt-in. With no cache option set, behaviour is exactly the historical default (unbounded in-memory caches).
For the cluster-scale Redis story, continue to Scaling to millions.
What is cached
| Cache | Scope | Default |
|---|---|---|
client.users | global | unbounded |
client.guilds | global | unbounded |
client.privateChannels | global | unbounded |
guild.members | per guild | unbounded |
guild.channels / threads / roles | per guild | unbounded |
guild.voiceStates / stageInstances | per guild | unbounded |
channel.messages | per channel | bounded by messageLimit (default 100) |
Within one process, users are not duplicated: client.users holds each user once and members/messages reference it. (With cache.transientMembers: false this dedup is intentionally relaxed; see the retention knobs below.) Duplication across separate cluster processes is inherent to multi-process sharding and only a shared store removes it (see Scaling to millions).
Free win: lazy containers (automatic)
Member allocates its roles, clientStatus, and activities containers lazily: a cached member whose roles are never read and who never receives a presence update allocates none of them (roughly 150 bytes saved per such member). The first read allocates once and stores the container, so identity (m.roles === m.roles) and in-place mutation behave exactly like eager fields.
Message does the same for mentions, roleMentions, attachments, embeds, reactions, stickerItems, and components: keys the payload omits allocate nothing until read, and keys the payload carries are adopted directly.
Both are automatic and semantically identical to eager fields. There is nothing to configure and no usage restrictions.
Config-only knobs
- Do not enable
GuildPresencesunless you use presence; it is the biggest source of churn. disableEvents: { TYPING_START: true, PRESENCE_UPDATE: true, VOICE_STATE_UPDATE: true }skips parsing events you ignore.- Lower
messageLimit, or set0to keep no messages.
Retention knobs (options.cache)
Behaviour switches that change WHAT gets cached, independent of which Collection backs each cache. All default to today's behaviour.
new Client(token, {
cache: {
guildEmojis: false, // don't retain raw emoji arrays per guild
guildStickers: false, // don't retain raw sticker arrays per guild
transientMembers: false, // don't cache members/users seen only via transient payloads
userSweepIntervalMs: 3_600_000 // hourly: evict users referenced by nothing
}
});guildEmojis/guildStickers(defaulttrue): whenfalse, the raw arrays fromGUILD_CREATE/GUILD_EMOJIS_UPDATE/GUILD_STICKERS_UPDATEare not stored on the guild (guild.emojis/guild.stickersstay[]). TheguildEmojisUpdate/guildStickersUpdateevents still receive the payload's arrays, so event-driven code keeps working.transientMembers(defaulttrue): whenfalse, members and users seen only through transient payloads (reactions, typing, message authors and mentions, interaction invokers and resolved data) are constructed for the event but not inserted intoguild.members/client.users. Already-cached entries still update in place, and member-lifecycle events (GUILD_MEMBER_ADD/UPDATE, member chunks, voice states) cache as always. This is the structural fix for member and user caches that grow forever with activity. The trade-off: a member who once reacted can no longer be assumed to be inguild.members; resolve later reads throughguild.fetchMember(id)(cache, then remote store, then REST).userSweepIntervalMs(default0= off): when above 0,client.sweepUsers()runs on that interval (unref'd timer), evicting users referenced by no cached guild member, DM recipient, or the bot user itself. Existing object references are never broken; eviction only affects futureclient.users.get()lookups, and a re-seen user is re-cached. You can also callclient.sweepUsers()manually.
One-flag preset: largeBotOptimizations
new Client(token, { largeBotOptimizations: true }), or ATHENA_LARGE_BOT=1 in the environment, applies a maintained bundle of large-scale defaults: currently rest.fullErrorStacks: false, cache.guildEmojis: false, cache.guildStickers: false, and cache.userSweepIntervalMs: 3_600_000. Explicit options always win over the preset, and the env variable lets a fleet flip it without a code change. The preset never sets transientMembers: that knob changes lookup semantics, so it stays your explicit call. Details in Client options.
Pluggable caches
Override how any cache is constructed. Unset entries keep the default.
import { Client, LRUCollection, NullCollection, User, Member, VoiceState, StageInstance } from 'athena';
new Client(token, {
cache: {
users: () => new LRUCollection(User, 100_000),
members: (guild) => new LRUCollection(Member, 50_000),
voiceStates: () => new NullCollection(VoiceState),
stageInstances: () => new NullCollection(StageInstance)
}
});LRUCollection(Class, max)keeps at mostmaxentries and evicts the least recently used. A successfulgetmarks an entry as recently used.NullCollection(Class)constructs and returns objects but stores nothing, soget/hasalways miss andsizestays 0. Use it to disable a cache you never read back. Only disable caches you do not read.
Overridable cache keys: users, guilds, privateChannels, members, channels, threads, roles, voiceStates, stageInstances, messages.
Choosing
- Small or medium bot: do nothing.
- Memory-conscious single process:
largeBotOptimizations: true, thenLRUCollectioncaps,NullCollectionfor unused caches, lowermessageLimit. - Huge multi-cluster bot: add
transientMembers: falseand a shared remote store on top; see Scaling to millions.