Peers & cache

Custom peer storage

Replace gogram's in-memory peer cache with a backing store of your choice. Useful for cold-starts on long-lived bots, multi-process deployments, and anything that wants to share state across instances.

Why persist the cache

The in-memory default works for a fresh process talking to a handful of peers. As soon as you have either of:

  • Cold starts on a serverless / autoscaled deployment — every instance has to re-resolve every peer.
  • Multiple processes for the same account — each one re-resolves independently.
  • A bot talking to thousands of peers — the memory footprint adds up.

...a shared, persistent store starts paying for itself.

The CACHE struct

gogram exposes the cache as a struct with public methods rather than an interface:

UpdateUser(*UserObj)
Insert or update a user entry.
UpdateChat(Chat)
Insert or update a chat or channel.
GetUser(id) (*UserObj, bool)
Lookup by id.
GetChat(id) (Chat, bool)
Lookup by id.
GetInputPeer(id) (InputPeer, error)
Build an InputPeer from whatever is cached.
GetPeersFromUpdate(updates) []Peer
Pull every peer mentioned in an update bundle.

To customise, embed *telegram.CACHE in your own type and override the methods you want to back with persistence. The embedded methods handle the in-memory paths; your overrides add the storage layer.

Plugging in your own

client, _ := telegram.NewClient(telegram.ClientConfig{
	AppID:   12345,
	AppHash: "...",
	Session: "bot.session",
	Cache:   myCache,
})
client, _ := telegram.NewClient(telegram.ClientConfig{
	AppID:   12345,
	AppHash: "...",
	Session: "bot.session",
	Cache:   myCache,
})

If you also want the in-memory hot path disabled (so every read goes to your store), set DisableCache: true in the config — the embedded CACHEbecomes a pass-through.

Example: SQLite

Read-through + write-through layered on top of the default in-memory cache:

type SQLiteCache struct {
	db *sql.DB
	*telegram.CACHE // embed for in-memory paths
}

func (s *SQLiteCache) UpdateUser(u *telegram.UserObj) {
	s.CACHE.UpdateUser(u)
	s.db.Exec(`INSERT INTO peers(id, kind, access_hash, username)
	           VALUES (?, 'user', ?, ?)
	           ON CONFLICT(id) DO UPDATE SET access_hash=excluded.access_hash, username=excluded.username`,
		u.ID, u.AccessHash, u.Username)
}

func (s *SQLiteCache) GetUser(id int64) (*telegram.UserObj, bool) {
	if u, ok := s.CACHE.GetUser(id); ok {
		return u, true
	}
	// fall back to disk
	var accessHash int64
	var username string
	row := s.db.QueryRow(`SELECT access_hash, username FROM peers WHERE id=? AND kind='user'`, id)
	if err := row.Scan(&accessHash, &username); err != nil {
		return nil, false
	}
	u := &telegram.UserObj{ID: id, AccessHash: accessHash, Username: username}
	s.CACHE.UpdateUser(u) // warm in-memory
	return u, true
}
type SQLiteCache struct {
	db *sql.DB
	*telegram.CACHE // embed for in-memory paths
}

func (s *SQLiteCache) UpdateUser(u *telegram.UserObj) {
	s.CACHE.UpdateUser(u)
	s.db.Exec(`INSERT INTO peers(id, kind, access_hash, username)
	           VALUES (?, 'user', ?, ?)
	           ON CONFLICT(id) DO UPDATE SET access_hash=excluded.access_hash, username=excluded.username`,
		u.ID, u.AccessHash, u.Username)
}

func (s *SQLiteCache) GetUser(id int64) (*telegram.UserObj, bool) {
	if u, ok := s.CACHE.GetUser(id); ok {
		return u, true
	}
	// fall back to disk
	var accessHash int64
	var username string
	row := s.db.QueryRow(`SELECT access_hash, username FROM peers WHERE id=? AND kind='user'`, id)
	if err := row.Scan(&accessHash, &username); err != nil {
		return nil, false
	}
	u := &telegram.UserObj{ID: id, AccessHash: accessHash, Username: username}
	s.CACHE.UpdateUser(u) // warm in-memory
	return u, true
}

Example: Redis

The pattern is the same. Use a hash like peer:<id> with fields access_hash, username, kind. Set a generous TTL (peers do rotate access hashes occasionally) and let the in-memory layer absorb hot reads.