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
InputPeerfrom 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.
