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

CacheScopeDefault
client.usersglobalunbounded
client.guildsglobalunbounded
client.privateChannelsglobalunbounded
guild.membersper guildunbounded
guild.channels / threads / rolesper guildunbounded
guild.voiceStates / stageInstancesper guildunbounded
channel.messagesper channelbounded 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 GuildPresences unless 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 set 0 to 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 (default true): when false, the raw arrays from GUILD_CREATE / GUILD_EMOJIS_UPDATE / GUILD_STICKERS_UPDATE are not stored on the guild (guild.emojis / guild.stickers stay []). The guildEmojisUpdate / guildStickersUpdate events still receive the payload's arrays, so event-driven code keeps working.
  • transientMembers (default true): when false, 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 into guild.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 in guild.members; resolve later reads through guild.fetchMember(id) (cache, then remote store, then REST).
  • userSweepIntervalMs (default 0 = 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 future client.users.get() lookups, and a re-seen user is re-cached. You can also call client.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 most max entries and evicts the least recently used. A successful get marks an entry as recently used.
  • NullCollection(Class) constructs and returns objects but stores nothing, so get/has always miss and size stays 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, then LRUCollection caps, NullCollection for unused caches, lower messageLimit.
  • Huge multi-cluster bot: add transientMembers: false and a shared remote store on top; see Scaling to millions.