localStorage vs sessionStorage vs Cookies: What Should a Web App Store Where?

A preference toggle remembers the wrong setting. A search filter vanishes after a refresh. An account stays signed in longer than expected. Many of these issues begin with one ordinary development choice: where should the browser keep a piece of data?

Modern web apps routinely use localStorage, sessionStorage, and cookies. They all store browser-related state, but their lifetimes, access rules, and security consequences are different. Choosing the right one improves reliability, simplifies debugging, and reduces accidental exposure of sensitive information.

Start with the question: Does this value need storage at all?

Before choosing an API, ask whether the value must survive a refresh or a new visit. A temporary open/closed menu flag can stay in JavaScript memory. A search filter that should be shared with another person might belong in the URL rather than in browser storage. A user preference, such as compact layout, may benefit from being saved locally.

Some data belongs on the server. A user’s account permissions, paid subscription status, and authoritative shopping-cart totals should never depend on a changeable value in browser storage. A server can remember an authenticated session while the browser holds only a session identifier.

A useful rule is to store the minimum information needed for the experience, for no longer than it is useful.

The three mechanisms at a glance

MechanismTypical lifetimeSent with HTTP requests automatically?Readable by page JavaScript?Common fit
localStorageNo built-in expiry; can be cleared or evictedNoYes, for the relevant originNon-sensitive preferences
sessionStorageScoped to a page session, usually a tabNoYes, for the relevant origin and tab contextTemporary tab-specific UI state
CookiesControlled by expiry and browser behaviorYes, when domain, path, and other cookie rules matchOften, unless HttpOnlyServer-managed sessions and certain preferences

These are broad patterns, not security guarantees. Private-browsing modes, user settings, storage policies, and quota restrictions can shorten storage lifetime. Cookies may be session cookies or persistent cookies; sessionStorage is not a synonym for a secure session. Web Storage is not sent automatically, but a script that reads it can transmit its contents. Saving data locally does not guarantee that it stays on the device.

localStorage: Convenient for preferences, not a vault

localStorage offers a synchronous key-value API. An origin combines the scheme, hostname and port; paths on the same origin share this storage. A page can save a string and read it back later, typically across visits to the same origin.

const KEY = "catalog:preferences:v1";

try {
  localStorage.setItem(KEY, JSON.stringify({
    compactCards: true,
    preferredSort: "recent"
  }));

  const raw = localStorage.getItem(KEY);
  const preferences = raw ? JSON.parse(raw) : {};
  console.log(preferences.preferredSort);
} catch {
  // Storage can be blocked, full, or contain invalid JSON.
  console.warn("Could not save or read preferences");
}

This example saves and reads a preference together. In an app, save when the user changes a preference and read it when the interface starts. Handle storage failures and invalid JSON in both paths, keep usable defaults, and validate the shape and allowed values after parsing. A key name with a version helps migrations: a later release can treat an older preference structure explicitly instead of assuming it is compatible.

The major security limitation is straightforward: scripts running with your page’s origin can read localStorage. If an attacker succeeds in injecting JavaScript through a cross-site scripting (XSS) vulnerability, values stored there may be accessible to that code. Do not use Web Storage to persist passwords or session identifiers. The OWASP HTML5 security guidance explains why JavaScript access makes local storage unsuitable for session identifiers.

If your app renders content from users or APIs, study HTML Escaping vs Sanitizing: When to Use Each before inserting markup into a page. Good output handling reduces XSS risk; it does not make localStorage a secure credential store.

sessionStorage: Useful for a tab, still visible to scripts

sessionStorage uses a similar interface, but its data is scoped to an origin within a top-level tab. It survives ordinary reloads. It is useful for a multistep interface, a temporary sort setting, or a draft that should not normally persist across future visits.

let step = 1;

try {
  const saved = Number(sessionStorage.getItem("wizard:step"));
  if (Number.isInteger(saved) && saved >= 1 && saved <= 5) {
    step = saved;
  }
  sessionStorage.setItem("wizard:step", String(step));
} catch {
  // Keep the in-memory step when storage is unavailable.
}

This five-step example accepts only a valid saved step and continues in memory if storage is blocked or full. Update the saved value when the user advances.

The important warning is that sessionStorage is not protected from XSS. Code running on the page can read it. A page opened with an opener can receive an initial copy of that tab’s session storage; subsequent changes remain separate. Browser restoration and tab duplication also make its lifetime less intuitive than simply “until the tab closes.” Do not promise users that closing a tab irreversibly destroys a sensitive draft.

Use tab-scoped storage for convenience, not as an access-control boundary. If a wizard includes sensitive information, decide whether the server needs to hold it securely instead, and include a clear expiration and deletion policy.

Cookies: The main option for server-managed browser sessions

Cookies are different because the browser automatically sends matching cookies with HTTP requests. This makes them useful for a server that recognizes a session identifier and checks permissions on every request.

A server might issue a Set-Cookie header like this over HTTPS:

Set-Cookie: __Host-session=opaque-session-id; Path=/; Secure; HttpOnly; SameSite=Lax

This is an illustrative cookie, not a session system you can copy without additional design. The server must create an unpredictable identifier, store or verify its server-side meaning, rotate it where appropriate, and expire or revoke it on logout. A __Host- cookie must be set over HTTPS with Secure, have Path=/, and omit Domain.

Without Max-Age or Expires, this is a session cookie. Browser session restoration may preserve it, so enforce idle and absolute session timeouts on the server. On logout, invalidate the server session and clear the matching cookie.

The attributes solve different problems:

  • Secure restricts transmission to HTTPS requests, with special handling for localhost; use HTTPS in production.
  • HttpOnly prevents direct reading through document.cookie, helping protect a session value from straightforward JavaScript access.
  • SameSite=Lax restricts many cross-site sends, but still permits top-level cross-site navigation with safe methods such as GET. It is not a complete defense against cross-site request forgery (CSRF); do not use GET for state-changing actions.
  • Path and Domain decide where a cookie is sent. They do not create a trustworthy authorization boundary on their own.

An HttpOnly cookie is not a cure for XSS. Injected script can still cause the browser to make authenticated same-origin requests or alter the page. Use appropriate CSRF protections for state-changing endpoints, in addition to input validation, output encoding, and an application-specific Content Security Policy. OWASP’s session management guide covers session expiration, cookie attributes and logout handling.

What should never be trusted as an authority?

All three mechanisms are under meaningful control of the client. Users can change local storage values, edit their own cookies, or send handcrafted requests. That is useful for testing, but it means storage cannot decide authorization.

Suppose an app stores role=admin in a cookie or localStorage. Showing an admin menu based on that value might change the interface, but the server must independently check the authenticated user’s permissions before allowing privileged actions. A hidden menu is not security.

Likewise, storing a cartTotal value can help render a preview, but the server should recalculate prices and discounts before taking payment.

When the URL is a better choice

If the value defines a shareable view, prefer a query parameter to hidden browser storage. Public search terms, pagination, public filter choices, and sort order are natural URL state. This keeps refreshes, bookmarks, and the Back button understandable.

The trade-off is exposure. A URL may appear in logs, history, and shared messages, so it should not contain secrets. The URL encoder, decoder, and query editor helps inspect repeated parameters and encoding, and Meet the New URL Encoder, Decoder, and Query Editor explains the + versus %2B distinction. Encoding makes a URL structurally correct; it does not hide its contents.

Inspect storage and requests in browser developer tools

For a small test, open a site in Chrome or Edge DevTools and select Application, or use the equivalent Storage panel in Firefox. Look at local storage, session storage, and cookies for the origin you are inspecting. Safari also provides storage inspection through Web Inspector, although labels and menus vary.

Then open Network and perform one action at a time. Local storage writes are not automatically network transmissions; cookies can be automatically attached to applicable outgoing requests. Compare a request before and after login, refresh, or logout. Examine the request headers, response Set-Cookie headers, and whether the server rejects a request after logout.

Web2Generators’ guide to checking website network requests across major browsers is a useful starting point. Use dummy accounts and sample values. Network captures, HAR exports, and screenshots may expose cookies or personal data, so review them carefully before sharing a bug report.

For HTTP tests outside the browser, the cURL command line builder can prepare a repeatable request. Its generated command does not automatically reproduce the entire cookie jar or browser security context. If you are comparing the two, account for redirects, cookie handling, and the browser’s credential mode.

A small decision guide for a real app

Imagine a documentation website with a theme switcher, search box, and account area.

Save a non-sensitive theme choice in localStorage if persistence is useful and permitted. Keep an unsaved, short-lived form step in memory or sessionStorage according to your recovery requirements. Put the public search query and page number in the URL so the result can be bookmarked. Let the server manage the authenticated session through a carefully configured cookie, rather than persisting passwords in browser storage.

Review your storage strategy when requirements change

A convenient local preference can become sensitive if a new feature starts storing account data in the same object. A login flow may need different CSRF controls when it moves from bearer tokens to cookies. A consent choice may need to change where and how long it is retained.

Document each stored value, its purpose, who can read it, how it expires, and what happens on sign-out. Test in more than one browser and under restricted-storage settings. That brief review often prevents more problems than adding another library.

Check what your app saves and sends

Open the browser network inspection guide, then test one preference change, one reload and one logout with a dummy account. Compare the Storage and Network panels after each action. Check that the preference survives only as intended and that a protected request using the old session is rejected after logout. Record any mismatch before adding more stored state.

Premium is coming soon

An optional premium subscription to remove ads is planned. Subscriptions are not available yet.

You can keep using our free tools without an account.