
How to use sessions in Astro?
To use sessions in Astro, make sure your project has an adapter and a session driver, then read and write data with Astro.session in on-demand pages, or context.session in endpoints, middleware and actions. await Astro.session?.get('cart') reads a value, Astro.session?.set('cart', cart) stores one, regenerate() gives the visitor a new session ID and destroy() deletes everything. Astro stores the data on the server using the driver you configure and sends the browser only a cookie containing a random session ID. With the Node, Netlify and Cloudflare adapters, a driver is configured for you, so sessions work without any extra setup.
Sessions solve a problem that cookies can't: keeping more than a few kilobytes of per-visitor data, or data you don't want the visitor to see or edit. Shopping carts, multi-step forms, flash messages and the ID of a signed-in user are classic examples. In this article you'll learn how sessions work in Astro, how to configure drivers and cookie options, the full session API, how to make session data type-safe, how to use sessions in pages, endpoints and actions, and how to handle login and logout safely.
How Astro Sessions Work
A session has two halves:
- A cookie in the browser: called
astro-sessionby default. It contains only a random ID, such as7207fe52-6bca-491f-8574-0912914b56b1. - Data on the server: stored by a driver under that ID, in a file, a key-value store, Redis or a database.
On each request, Astro reads the cookie, loads the matching data from storage the first time you call get(), and lets you change it with set() and delete(). Changes are made in memory during the request and saved to the driver at the end of the request. If the visitor has no session yet, one is created the first time you use it.
Because only the ID leaves the server, sessions avoid the roughly 4 KB size limit on cookies, and the visitor can't read or forge the data itself. The post on reading and setting cookies in Astro covers the cookie side in detail.
What You Need
Sessions require on-demand rendering. Astro.session is undefined on prerendered pages, when no driver is configured, or when sessions are disabled. That's why every example uses optional chaining (Astro.session?.get()). If you're new to on-demand pages, start with enabling server-side rendering in Astro.
The examples in this article were built and run with Astro 7.4.2 and @astrojs/node.
Configuring a Session Driver
Adapters That Configure Sessions for You
The Node, Netlify and Cloudflare adapters set up a default driver when you don't provide one. With @astrojs/node, the build log tells you:
[@astrojs/node] Enabling sessions with filesystem storage
The Node adapter stores sessions as files in a sessions folder inside Astro's cache directory (node_modules/.astro by default). That's fine for a single server, but every instance needs to see the same data, so switch to a shared store such as Redis when you run more than one. Netlify uses Netlify Blobs and Cloudflare uses Workers KV. Other adapters, including Vercel, require you to choose a driver yourself.
Choosing a Driver
Drivers come from the sessionDrivers helper exported by astro/config. They're built on the unstorage library, so you get a long list of options:
// astro.config.mjs
import { defineConfig, sessionDrivers } from 'astro/config';
import vercel from '@astrojs/vercel';
export default defineConfig({
adapter: vercel(),
session: {
driver: sessionDrivers.redis({
url: process.env.REDIS_URL,
}),
},
});
Some useful choices:
| Driver | Good for |
|---|---|
sessionDrivers.fsLite() / fs() | A single Node server with a writable disk |
sessionDrivers.redis() | Multiple servers sharing a Redis instance |
sessionDrivers.upstash() | Serverless platforms with Upstash Redis |
sessionDrivers.netlifyBlobs() | Netlify |
sessionDrivers.cloudflareKVBinding() | Cloudflare Workers KV |
sessionDrivers.lruCache() | Local testing; data is lost on restart |
sessionDrivers.mongodb() / planetscale() / db0() | Storing sessions in a database |
Each driver takes the same options as its unstorage counterpart, and some options are required, so check the unstorage documentation for the driver you pick. You may see older configs that pass a string such as driver: 'redis' with a separate options object. That form is deprecated and is scheduled for removal in Astro 8, so use the sessionDrivers helpers.
Configuring the Driver at Runtime
Session drivers are configured at build time, and any environment variables you reference in astro.config.mjs are inlined into the build. To read connection details at runtime instead, point the driver at your own module that returns an object with getItem, setItem and removeItem:
// src/session-driver.ts
import type { SessionDriver } from 'astro';
import redisDriver from 'unstorage/drivers/redis';
import { REDIS_HOST, REDIS_PORT } from 'astro:env/server';
export default function (): SessionDriver {
const driver = redisDriver({ host: REDIS_HOST, port: REDIS_PORT });
return {
async getItem(key) {
return await driver.getItem(key);
},
async setItem(key, value) {
await driver.setItem?.(key, value, {});
},
async removeItem(key) {
await driver.removeItem?.(key, {});
},
};
}
// astro.config.mjs
export default defineConfig({
session: {
driver: {
entrypoint: new URL('./src/session-driver.ts', import.meta.url),
},
},
});
The astro:env/server import keeps those values typed and server-only, as explained in using astro:env for type-safe environment variables.
Cookie Name and Lifetime
Two more options sit next to driver:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
adapter: node({ mode: 'standalone' }),
session: {
cookie: { name: 'sid', sameSite: 'lax' },
ttl: 60 * 60 * 24 * 7,
},
});
- cookie: a string to rename the cookie, or an object with
nameplus cookie options such assameSite,path,domain,secureandmaxAge.httpOnlyandexpiresare controlled by Astro and can't be changed. - ttl: the default lifetime of session data, in seconds. Without it, data is kept until it's deleted or the cookie expires.
With that config, the first request that touched the session returned:
set-cookie: sid=7207fe52-6bca-491f-8574-0912914b56b1; Path=/; HttpOnly; Secure; SameSite=Lax
The cookie is HttpOnly and Secure by default. Browsers treat localhost as a secure context, so this still works during local development.
Turning Sessions Off
If you use the Node, Netlify or Cloudflare adapter but don't need sessions, set session: false. Since Astro 7.2 this stops the adapter from wiring up its default driver and leaves the session runtime out of the server bundle, which can shave a little off cold starts on serverless and edge platforms:
// astro.config.mjs
export default defineConfig({
session: false,
});
The Session API
Astro.session and context.session share the same methods:
| Method | What it does |
|---|---|
get(key) | Returns a promise with the value, or undefined |
set(key, value, { ttl }) | Stores a value; ttl (seconds) is optional |
delete(key) | Removes one key |
has(key) | Resolves to true if the key exists |
keys(), values(), entries() | Iterate over the stored data |
regenerate() | Moves the data to a new session ID |
destroy() | Deletes the data and clears the cookie |
load(id) | Loads a session by ID instead of from the cookie |
Note the asymmetry: reading is asynchronous because it may hit storage, but set() and delete() are synchronous. They update the in-memory copy, and Astro saves it once the response is ready.
Values are serialised with devalue, so you can store strings, numbers, arrays, plain objects, Date, Map, Set and URL values and get the same types back.
Making Session Data Type-Safe
By default, get() returns any. Declare the shape of your session in src/env.d.ts and Astro uses it everywhere:
// src/env.d.ts
declare namespace App {
interface SessionData {
cart: { id: string; qty: number }[];
userId: string;
flash: string;
}
}
Now await Astro.session?.get('cart') is typed as { id: string; qty: number }[] | undefined, and set('cart', 'oops') is a type error. These types exist only at compile time, though. If you change a type after visitors already have stored data, old sessions will still contain the old shape, so handle that defensively when you deploy changes.
Using Sessions in Pages
Here's a cart page that reads the cart, shows a one-time flash message and then removes it:
---
// src/pages/cart.astro
export const prerender = false;
const cart = (await Astro.session?.get('cart')) ?? [];
const flash = await Astro.session?.get('flash');
if (flash) Astro.session?.delete('flash');
const count = cart.reduce((sum, item) => sum + item.qty, 0);
---
<html lang="en">
<body>
{flash && <p class="flash">{flash}</p>}
<p>{count} items in your cart</p>
<ul>
{cart.map((item) => <li>{item.id} × {item.qty}</li>)}
</ul>
</body>
</html>
Flash messages are a neat use of sessions: you set a message in one request, show it on the next page and delete it, so it appears exactly once.
Using Sessions in Endpoints
Endpoints receive session in their context. This one adds an item to the cart and sets a flash message:
// src/pages/api/cart.ts
import type { APIRoute } from 'astro';
export const prerender = false;
export const POST: APIRoute = async ({ request, session }) => {
const { id } = (await request.json()) as { id?: string };
if (!id) return new Response('id is required', { status: 400 });
const cart = (await session?.get('cart')) ?? [];
const existing = cart.find((item) => item.id === id);
if (existing) existing.qty += 1;
else cart.push({ id, qty: 1 });
session?.set('cart', cart);
session?.set('flash', `Added ${id} to your cart`);
return Response.json(cart);
};
export const DELETE: APIRoute = ({ session }) => {
session?.delete('cart');
return new Response(null, { status: 204 });
};
Posting mug, mug and tee with the same cookie jar returned a growing cart:
[{"id":"mug","qty":1}]
[{"id":"mug","qty":2}]
[{"id":"mug","qty":2},{"id":"tee","qty":1}]
Then loading /cart rendered the data and the flash message, and a second load showed the cart without the message:
<p class="flash">Added tee to your cart</p><p>3 items in your cart</p><ul><li>mug × 2</li><li>tee × 1</li></ul>
With the Node adapter's default driver, the data sat in a file named after the session ID in node_modules/.astro/sessions, serialised with devalue.
Using Sessions in Actions and Middleware
In Astro Actions, the handler's second argument is the context, which includes session:
// src/actions/index.ts
import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';
export const server = {
addToCart: defineAction({
input: z.object({ id: z.string() }),
handler: async ({ id }, context) => {
const cart = (await context.session?.get('cart')) ?? [];
cart.push({ id, qty: 1 });
context.session?.set('cart', cart);
return cart.length;
},
}),
};
In middleware, context.session works the same way, which makes it a good place to load the current user from a stored ID and put it in locals. The one limitation is edge middleware: sessions aren't supported in middleware that runs on an edge runtime.
Login and Logout
Sessions are the standard way to remember a signed-in user, and two methods exist specifically for this.
Regenerate on Login
When a user signs in or their privileges change, call regenerate() before storing their ID:
// src/pages/api/login.ts
import type { APIRoute } from 'astro';
export const prerender = false;
export const POST: APIRoute = async ({ session, redirect }) => {
// ...verify the user's credentials here...
await session?.regenerate();
session?.set('userId', 'user_123');
return redirect('/cart', 303);
};
This protects against session fixation, an attack where someone plants a known session ID in a victim's browser and waits for them to log in. After regenerate(), the old ID is useless. In testing, the response set a brand-new cookie, the old session file was removed, and the cart from before login was still there under the new ID:
set-cookie: sid=098bc80d-f334-4279-bf87-690773d25c91; Path=/; HttpOnly; Secure; SameSite=Lax
<p>3 items in your cart</p>
Destroy on Logout
On logout, destroy() deletes the stored data and clears the cookie:
// src/pages/api/logout.ts
import type { APIRoute } from 'astro';
export const prerender = false;
export const POST: APIRoute = ({ session, redirect }) => {
session?.destroy();
return redirect('/', 303);
};
set-cookie: sid=deleted; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax
The session file was gone from storage afterwards. For a complete sign-in system with password hashing and OAuth, use a library as described in adding user authentication to an Astro site, and use sessions for your own app data alongside it.
Sessions vs Cookies
| Cookies | Sessions | |
|---|---|---|
| Where data lives | In the browser | On the server |
| Size limit | About 4 KB per cookie | Limited by your storage |
| Visible to the visitor | Yes | No, only the ID |
| Works on prerendered pages via JS | Yes, if not HttpOnly | No |
| Needs storage infrastructure | No | Yes, a driver |
| Good for | Theme, language, consent | Carts, user IDs, form state, flash messages |
FAQ: Sessions in Astro
The page is prerendered, no session driver is configured, or sessions are disabled with session false. Render the page on demand and use the Node, Netlify or Cloudflare adapter, or configure a driver in session.driver.
In the storage driver you configure. The Node adapter defaults to files in node_modules/.astro/sessions, Netlify uses Netlify Blobs and Cloudflare uses Workers KV. The browser only holds a cookie with the session ID.
No. set and delete update the session synchronously in memory, and Astro saves the data to storage at the end of the request. get is asynchronous because it may need to load data from storage.
Declare an App.SessionData interface in src/env.d.ts with a key for each value you store. get and set then use those types, although the data itself isn't validated at runtime.
Call it whenever a user signs in or their privileges change. It moves the data to a new session ID, which prevents session fixation attacks that rely on a known, pre-planted ID.
Yes, but the Vercel adapter doesn't configure a driver for you. Choose one that works across serverless functions, such as Redis or Upstash, and set it in session.driver.
Conclusion
Astro sessions give you server-side storage for per-visitor data with a very small API: get(), set(), delete(), regenerate() and destroy() on Astro.session or context.session. The browser only holds an HttpOnly cookie with a random ID, and the data lives in a driver, which the Node, Netlify and Cloudflare adapters configure automatically.
Declare App.SessionData to get types, pick a shared driver such as Redis once you run more than one server, call regenerate() on login and destroy() on logout. With those habits, sessions become the natural home for carts, flash messages, multi-step forms and signed-in user IDs in any on-demand Astro site.


