Entity cache
Every "send a message to @username" hides a lookup: username to id + access_hash to InputPeer. The cache is what makes that lookup free the second time. Here's the actual data structure, its bounds, and the failure modes.
Why a cache exists at all#
Telegram won't accept a bare user id in a request. Almost every method wants an InputPeer carrying the access_hash the server issued to your accountwhen it first showed you that peer — proof you've legitimately seen them. The server attaches full user/chat objects to responses and updates precisely so clients can harvest those hashes. Resolving on the network instead (contacts.resolveUsername) is rate-limited hard enough that a resolve-per-message bot flood-waits within minutes. The cache is not an optimization; it's how the protocol is meant to be consumed.
Two layers#
client.getInputEntity("@someone")
1. in-memory cache client.entityCache hit -> done, 0 network
2. session store session.getInputEntity hit -> done, 0 network
(may be async: Redis/Postgres-backed sessions)
3. network contacts.resolveUsername / users.getUsers
response entities feed processEntities,
which updates layers 1 and 2Layer 1 is the in-memory cache on the client object — fast, bounded, gone on restart. Layer 2 is the session's entity store — survives restarts with StoreSession, and since v1.227.2 its hooks may return Promises, so it can live in Redis or Postgres with no local mirror. Every TL object that flows through the client (responses and pushed updates) passes through processEntities, which writes both layers. A storage error in layer 2 is logged and swallowed — a broken Redis never fails the request that triggered the write.
What a record holds#
Pre-1.227 the cache kept entire TL objects — a User with every profile field, photo references, status. The rewrite stores compact records instead: id, access_hash, kind (user / chat / channel), username, phone — the fields resolution actually needs, a few dozen bytes instead of kilobytes. Usernames and phone numbers index into the same records, so getInputEntity("@name"), getInputEntity("+15551234") and getInputEntity(id) all hit the same entry.
Full objects (what getEntity returns) are not cached long-term — if you need fresh profile fields, you pay the network call, by design.
LRU, segments, TTL#
The in-memory layer is segmented by peer kind, each segment capped at 4096 records with least-recently-used eviction. A userbot that scrolls a large group sees thousands of members stream through processEntities; before the bound, that heap growth was permanent — the classic "my bot uses 2 GB after a week" report. Now cold peers fall out the back, and the peers your code actually touches stay pinned by recency.
TTL is optional and off by default. Switch it on when you'd rather re-resolve than act on old data — the trade is a network round-trip on expiry versus a rare stale hash (see rotationbelow for why stale hashes mostly don't matter anymore).
Tuning entityCache#
const client = new TelegramClient(session, apiId, apiHash, {
connectionRetries: 5,
// default: bounded — LRU, 4096 peers per segment
// entityCache: true,
// unbounded (pre-1.227 behavior): grows with every peer ever seen
// entityCache: {},
// explicit bounds: cap size, expire records after 30 min
entityCache: { max: 10_000, ttl: 30 * 60 * 1000 },
// off: every resolution goes to the session store or the network
// entityCache: false,
});
// runtime inspection
console.log(client.entityCache);Defaults are right for almost everyone. Raise max for fan-out bots that legitimately address tens of thousands of distinct peers per hour (the eviction churn shows up as repeated session-store reads). Pass {}only if you've measured that unbounded growth is acceptable for your process lifetime.
Rotation & staleness#
Telegram occasionally rotates a peer's access_hash. Old behavior: the cache kept the first hash it ever saw, requests started failing, and the fix was "delete the session". Since v1.227.2 a hash arriving in fresh server data replaces the cached one. The remaining genuinely unfixable case is cross-account reuse — a hash is derived from your auth key, so records copied between sessions are garbage. Never ship entity data between accounts; let each session harvest its own. Resolution failures and their causes are covered in Entities & Peers.