| name | sessions |
| description | This skill should be used when the user asks about "grammY session", "store user state", "ctx.session", "session storage", "Redis session", "MongoDB session", "PostgreSQL session", "free session storage", "lazy session", "multi session", or needs to persist per-chat data across messages. |
grammY — Sessions
The session middleware attaches a ctx.session object to every chat. Modify it freely in your handlers — grammY persists it after each update. Choose an in-memory store for development, a real database for production, or grammY's free hosted storage for hobby bots.
Type the session shape
Always declare SessionData and a SessionFlavor-augmented context:
import { Bot, Context, SessionFlavor, session } from "grammy";
interface SessionData {
counter: number;
language: string;
state: "idle" | "awaiting_name" | "awaiting_email";
}
type MyContext = Context & SessionFlavor<SessionData>;
const bot = new Bot<MyContext>(process.env.BOT_TOKEN!);
In-memory (development only)
bot.use(session({
initial: (): SessionData => ({
counter: 0,
language: "en",
state: "idle",
}),
}));
bot.on("message", (ctx) => {
ctx.session.counter++;
return ctx.reply(`Counter: ${ctx.session.counter}`);
});
MemorySessionStorage is the default — data is lost on process restart. Never use in production.
Free hosted storage (zero-config production)
For hobby bots without infrastructure, use grammY's free key-value storage (rate-limited, intended for small bots):
npm install @grammyjs/storage-free
import { freeStorage } from "@grammyjs/storage-free";
bot.use(session({
initial: (): SessionData => ({ counter: 0, language: "en", state: "idle" }),
storage: freeStorage<SessionData>(bot.token),
}));
Free storage is keyed by your bot token — different bots get isolated namespaces automatically.
External storages
Install the storage adapter for your database:
| Database | Package |
|---|
| Redis | @grammyjs/storage-redis (uses ioredis) |
| MongoDB | @grammyjs/storage-mongodb |
| PostgreSQL | @grammyjs/storage-psql |
| Supabase | @grammyjs/storage-supabase |
| File | @grammyjs/storage-file |
| DenoKV | @grammyjs/storage-denokv |
| Cloudflare KV | @grammyjs/storage-cloudflare |
| Deno Deploy KV | @grammyjs/storage-denokv |
| AWS DynamoDB | @grammyjs/storage-dynamodb |
Redis example
import { RedisAdapter } from "@grammyjs/storage-redis";
import IORedis from "ioredis";
const redis = new IORedis(process.env.REDIS_URL!);
bot.use(session({
initial: () => ({ counter: 0 } as SessionData),
storage: new RedisAdapter({ instance: redis }),
}));
MongoDB example
import { MongoDBAdapter } from "@grammyjs/storage-mongodb";
import { MongoClient } from "mongodb";
const client = new MongoClient(process.env.MONGO_URI!);
await client.connect();
const collection = client.db("mybot").collection<{ key: string; value: SessionData }>("sessions");
bot.use(session({
initial: () => ({ counter: 0 } as SessionData),
storage: new MongoDBAdapter({ collection }),
}));
Session key — per chat vs per user
By default getSessionKey: (ctx) => ctx.chat?.id.toString() — one session per chat. Switch to per-user (across all chats) like this:
bot.use(session({
initial: () => ({ counter: 0 } as SessionData),
getSessionKey: (ctx) => ctx.from?.id.toString(),
}));
Or per (user, chat) pair:
getSessionKey: (ctx) =>
ctx.from && ctx.chat ? `${ctx.from.id}:${ctx.chat.id}` : undefined,
Return undefined to skip session loading for that update.
Lazy sessions — defer the read
Default session always reads from storage before middleware runs. lazySession only reads when you await ctx.session:
import { lazySession, type LazySessionFlavor } from "grammy";
type MyContext = Context & LazySessionFlavor<SessionData>;
bot.use(lazySession({
initial: () => ({ counter: 0 } as SessionData),
storage: new RedisAdapter({ instance: redis }),
}));
bot.on("message:text", async (ctx) => {
if (!ctx.message.text.startsWith("/admin")) return;
const data = await ctx.session;
data.counter++;
});
Use lazy sessions when most updates don't touch state — big win on read-heavy bots.
You can also assign a promise to ctx.session; grammY awaits it before writing back:
ctx.session = ctx.session.then((s) => {
s.counter = 0;
return s;
});
Multi-sessions — split data into separate storages
Different fragments of session data can live in different backends — e.g. hot counters in Redis, cold preferences in MongoDB:
bot.use(session({
type: "multi",
counters: {
initial: () => ({ hits: 0 }),
storage: new RedisAdapter({ instance: redis }),
},
prefs: {
initial: () => ({ language: "en" }),
storage: new MongoDBAdapter({ collection: prefsColl }),
},
}));
bot.on("message", (ctx) => {
ctx.session.counters.hits++;
ctx.session.prefs.language;
});
Each fragment can have its own getSessionKey, prefix, and initial.
Session migrations — add a field safely
When you add a new field to SessionData, existing stored sessions don't have it. Handle this in initial (called on miss) AND defensively in handlers, or run a one-time migration script.
interface SessionData {
counter: number;
language: string;
notifications: boolean;
}
bot.use(session({
initial: () => ({ counter: 0, language: "en", notifications: true }),
}));
bot.on("message", (ctx) => {
ctx.session.notifications ??= true;
});
When NOT to use sessions
- Multi-step user input wizards — use the
conversations plugin instead. It snapshots the entire control flow, not just data, so you can await conversation.wait() in normal-looking code.
- Anything global — session is per-chat or per-user. Global config belongs in env vars or a regular DB table you read directly.
- Long-lived large blobs — session is rewritten on every update. Keep it under a few KB; for larger data, store an ID in session and the blob in a separate table.