Skip to content
Astro as rendering 4 min read

Cookies & Sessions

Cookies are the foundation of stateful interactions on the web — authentication tokens, theme preferences, consent flags, and shopping carts all flow through them. Because cookies are read and written per request, they only make sense in on-demand (server-rendered) pages, never in pre-rendered static HTML. Astro exposes a clean Astro.cookies API for this, and modern Astro layers a higher-level session abstraction on top so you can persist server-side data keyed by an automatically managed cookie. This page covers both.

Reading and writing cookies

In any server-rendered .astro page or endpoint, the global Astro object exposes Astro.cookies. It works on the request’s incoming Cookie header for reads and queues Set-Cookie headers for the response on writes. Reading happens during the component script; writing must happen before any HTML is streamed.

---
// src/pages/dashboard.astro
export const prerender = false; // ensure on-demand rendering

const theme = Astro.cookies.get("theme")?.value ?? "light";

// Persist a visit counter
const raw = Astro.cookies.get("visits")?.number() ?? 0;
Astro.cookies.set("visits", String(raw + 1), {
  path: "/",
  httpOnly: false,
  sameSite: "lax",
  maxAge: 60 * 60 * 24 * 30, // 30 days
});
---

<html data-theme={theme}>
  <body>
    <p>You have visited {raw + 1} times.</p>
  </body>
</html>

The get() method returns an AstroCookie object with helpers rather than a bare string. This lets you coerce the value safely.

MethodReturnsNotes
Astro.cookies.get(key)AstroCookie | undefined.value for the raw string
cookie.json()anyParses the value as JSON
cookie.number()numberCoerces to a number
cookie.boolean()booleanCoerces to a boolean
Astro.cookies.has(key)booleanTrue if the cookie exists
Astro.cookies.set(key, value, opts)voidQueues a Set-Cookie header
Astro.cookies.delete(key, opts)voidExpires the cookie immediately
Astro.cookies.merge(headers)voidImports cookies from another Headers

The options object mirrors the standard Set-Cookie attributes:

Astro.cookies.set("session_id", token, {
  path: "/",
  httpOnly: true,   // hide from document.cookie / JS
  secure: true,     // HTTPS only
  sameSite: "lax",  // "strict" | "lax" | "none"
  maxAge: 3600,     // seconds
  // expires: new Date(...), // alternative absolute date
});

Always set httpOnly: true and secure: true for anything sensitive like session or auth tokens. An httpOnly cookie cannot be read by client-side JavaScript, which neutralizes a large class of XSS-based token theft.

You can serialize structured data with json() on read and JSON.stringify on write:

Astro.cookies.set("cart", JSON.stringify({ items: 3 }), { path: "/" });
const cart = Astro.cookies.get("cart")?.json();

Cookies in API endpoints

Endpoints receive cookies on the APIContext, so the same API is available outside .astro files. This is the idiomatic place to handle a login or logout.

// src/pages/api/login.ts
import type { APIRoute } from "astro";

export const POST: APIRoute = async ({ request, cookies, redirect }) => {
  const form = await request.formData();
  const token = await authenticate(form.get("email"), form.get("password"));

  cookies.set("session_id", token, {
    path: "/",
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    maxAge: 60 * 60 * 24 * 7,
  });

  return redirect("/dashboard", 302);
};

To log out, expire the cookie:

// src/pages/api/logout.ts
import type { APIRoute } from "astro";

export const POST: APIRoute = ({ cookies, redirect }) => {
  cookies.delete("session_id", { path: "/" });
  return redirect("/", 302);
};

Managing sessions

Storing everything in cookies has limits: a 4KB size cap and the fact that the data lives on the client. Astro’s session API solves this by keeping data server-side in a configured storage driver and handing the browser only a small opaque session-id cookie. Enable it in your config:

// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";

export default defineConfig({
  output: "server",
  adapter: node({ mode: "standalone" }),
  session: {
    driver: "fs", // any unstorage driver: redis, cloudflare-kv, etc.
  },
});

Then use Astro.session anywhere a request is being rendered. It is an async key-value store backed by the driver above.

---
// src/pages/cart.astro
export const prerender = false;

const cart = (await Astro.session?.get("cart")) ?? [];

if (Astro.request.method === "POST") {
  const form = await Astro.request.formData();
  cart.push(form.get("sku"));
  await Astro.session?.set("cart", cart);
}
---
<p>{cart.length} item(s) in your cart.</p>

The session driver is provided by unstorage, so you can swap the filesystem for Redis, Cloudflare KV, or Upstash without touching your page code — only the driver and options in config change.

Output:

2 item(s) in your cart.

The session API is experimental in earlier Astro 5 releases and graduated to stable in later ones — check your version. Sessions require an adapter and on-demand rendering; they do nothing on pre-rendered pages.

Best practices

  • Set export const prerender = false (or run in output: "server") on any page that touches cookies or sessions — static pages cannot read per-request state.
  • Mark auth and session cookies httpOnly, secure, and sameSite: "lax" (or "strict") to defend against XSS and CSRF.
  • Keep cookies small; push anything larger than a token or a flag into the session store instead of cookie payloads.
  • Always pass the same path to delete() that you used in set(), or the deletion silently misses the cookie.
  • Use cookie.json() / cookie.number() instead of hand-parsing value to avoid coercion bugs.
  • Choose a durable session driver (Redis, KV) in production; the fs driver is fine for local development but not for multi-instance deployments.
Last updated June 14, 2026
Was this helpful?