localStorage vs sessionStorage vs IndexedDB vs Cookies — Almacenamiento del navegador sin el arrepentimiento de las 3 de la madrugada
Una guía práctica para elegir el almacenamiento adecuado del navegador — con una tabla de comparación, el debate sobre JWT explicado claramente, y las maneras específicas en las que cada opción arruinará tu noche.
Son las 3 de la madrugada 😬. Un usuario ha reportado un error: su carrito de compras está vacío. Abres DevTools, haces clic en la pestaña Aplicación y miras fijamente el panel lateral. ¿Dónde lo puso? La respuesta correcta depende de una serie de preguntas que la mayoría de los desarrolladores solo consideran tras que se presenta el error.
Este no es un artículo de definiciones. Puedes encontrarlos en cualquier lugar. Este es el apartado donde hablamos de lo que falla, de qué usar realmente y por qué el debate sobre almacenar JWT en localStorage en realidad omite el punto principal.
La versión breve (a.k.a. la tabla que te bookmarkearás)
| localStorage | sessionStorage | IndexedDB | Galletas | |
|---|---|---|---|---|
| Persistencia | Permanente | Sesión de pestaña solo | Permanente | Configurable (sesión o fecha de vencimiento) |
| Tamaño máximo | 5–10 MB | 5–10 MB | GB (cuota del navegador) | Aproximadamente 4 KB por cookie |
| Acceso al servidor | No | No | No | Sí — se envía con cada solicitud |
| API asíncrona | No (bloquea el hilo principal) | No (bloquea el hilo principal) | Sí (basada en Promesa/evento) | No |
| Legible en JavaScript | Sí | Sí | Sí | Solo si no tiene la bandera HttpOnly |
| Acceso desde Web Worker | No | No | Sí | No |
| Compartido entre pestañas | Sí | No — cada pestaña está aislada | Sí | Sí |
| Evicto de Safari ITP | Después de 7 días sin interacción | Al cerrar la pestaña | Después de 7 días sin interacción | Depende del atributo Expires |
localStorage
Persistente, sincronizado, con ámbito de origen. El trabajo duro que todos usan y que, en mitad de los casos, no debería usarse.
Lo que realmente es
localStorage almacena pares clave-valor en forma de cadena. Eso es todo. El límite de almacenamiento es de 5 MB en la mayoría de los navegadores, 10 MB en algunos (Chrome te da más). Está limitado al origen — protocolo + dominio + puerto — por lo tanto http://example.com y https://example.com tienen almacenamiento separado. Sobrevive al cierre de pestaña, al reinicio del navegador, todo excepto cuando el usuario borra sus datos del navegador o cuando llamas explícitamente localStorage.clear().
// Read/write is synchronous — it happens right now, on the main thread
localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme'); // 'dark'
// Storing objects? You're serializing manually.
localStorage.setItem('user', JSON.stringify({ id: 1, name: 'Alex' }));
const user = JSON.parse(localStorage.getItem('user'));
Dónde falla
- Límite excedido — lanza un error sincronizado
DOMException: QuotaExceededError. Si no envuelves las escrituras en try/catch, te darás cuenta mediante un informe de error del usuario. - Privado/incógnito — los navegadores te dan un almacenamiento local fresco e aislado con una cuota más estricta (o cero). Firefox históricamente lanzaba errores de cuota inmediatamente. Nunca confíes en que localStorage esté disponible sin detectarlo previamente.
- Safari ITP — si un usuario no ha visitado tu sitio en los últimos 7 días, Safari puede eliminar localStorage. Este es un comportamiento documentado. Te sorprenderá en el momento más inoportuno.
- XSS — cualquier contenido en localStorage es legible por cualquier JavaScript que ejecute en tu origen. Si un atacante puede inyectar un script, obtiene todo.
No (POST → GET)
Preferencias de interfaz (modo oscuro, estado del panel lateral, idioma), caché no sensible (hora de última consulta, configuración estática), cualquier cosa que debería sobrevivir a una actualización de página pero que no contiene tokens de autenticación ni datos personales. Si estás pensando en almacenar un JWT o una clave de API aquí — sigue leyendo.
sessionStorage
Todo lo que localStorage hace, pero con una vida más corta y una característica de aislamiento crucial que molesta constantemente a los usuarios.
La trampa de aislamiento por pestaña
sessionStorage es por pestaña, no por navegador. Abrir la misma página en una nueva pestaña te da un almacenamiento local completamente separado. Esto es no obvio para los usuarios, y no será obvio para ti hasta que alguien reporte un error sobre que los datos de un formulario multietapa desaparecen cuando “accidentalmente” abre una segunda pestaña.
La única excepción: si el usuario abre una nueva pestaña mediante window.open() o haciendo clic con el botón del medio en un enlace, la nueva pestaña recibe una copia del almacenamiento local de la pestaña padre en el momento de la apertura. Después de eso, las dos están aisladas. Este es el tipo de caso extremo que genera una excelente pregunta en Stack Overflow a las 2 de la madrugada.
// Perfect for checkout flows — step data lives until the tab closes
sessionStorage.setItem('checkoutStep', '2');
sessionStorage.setItem('cartSnapshot', JSON.stringify(cart));
// Cleared automatically when the tab closes — no cleanup code needed
No (POST → GET)
Estado de formularios multietapa, datos de wizard de uso único, cualquier cosa que debería existir durante una sesión y desaparecer cuando el usuario cierra la pestaña. Es útil en flujos de pago — no quieres que el estado parcial de un pago se quede en una sesión anterior. No lo uses si necesitas datos compartidos entre pestañas o páginas abiertas independientemente.
IndexedDB
La opción madura. Asincrónica, transaccional y capaz de almacenar objetos reales de JavaScript — no solo cadenas serializadas. También tiene la API nativa más dolorosa de las tres, por eso casi nadie la usa directamente.
Lo que realmente es
IndexedDB es un almacén completo de pares clave-valor con soporte para índices y consulta basada en cursores. Los límites de almacenamiento son generosos — los navegadores permiten un porcentaje del espacio en disco, típicamente gigabytes en la práctica. Puedes almacenar objetos estructurados, Blobs, ArrayBuffers y Archivos sin tener que serializar manualmente. Está disponible en Web Workers. Es lo que usan las aplicaciones PWA y las aplicaciones offline para almacenar datos que serían inadecuados para mantener en memoria.
// Native IDB API — nobody writes this directly in production
const request = indexedDB.open('myDB', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
db.createObjectStore('users', { keyPath: 'id' });
};
request.onsuccess = (event) => {
const db = event.target.result;
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
store.put({ id: 1, name: 'Alex', avatar: someBlob });
};
// What you actually use — Dexie.js or the idb wrapper
import Dexie from 'dexie';
const db = new Dexie('myApp');
db.version(1).stores({ users: '++id, name, email' });
await db.users.add({ name: 'Alex', email: 'alex@example.com' });
Dónde falla
- Safari ITP — igual que localStorage: después de 7 días sin interacción, Safari puede eliminar los datos de IndexedDB. Esto eliminó varias aplicaciones PWA antes que Apple corrigiera el comportamiento en las versiones más recientes de iOS. Si tu audiencia incluye usuarios de Safari en iOS, tenlo en cuenta.
- Bajo almacenamiento en iOS — en iOS, el sistema operativo puede eliminar los datos de IndexedDB cuando el almacenamiento es bajo. No lo hará preguntar. Los datos simplemente no estarán allí.
- Navegación privada — Chrome permite IndexedDB en modo incógnito (con una cuota por sesión). Safari en navegación privada solía lanzar errores; el comportamiento varía según la versión.
- La API nativa — si escribes código raw de IDB sin un envoltorio, el modelo basado en eventos y callbacks producirá errores que te harán pasar una tarde en depuración. Usa idb o Dexie.js.
No (POST → GET)
Aplicaciones offline, conjuntos de datos grandes que sobrepasarían el límite de localStorage, cualquier cosa que construyas en localStorage que esté constantemente golpeando el límite de 5 MB, caché de archivos en PWAs. Si estás almacenando 50 documentos generados por usuarios localmente, IndexedDB es la solución. Si estás almacenando una preferencia de tema del usuario, es excesivo.
Galletas
La más antigua de las cuatro. La única que ve el servidor. La única con un límite de 4 KB que te golpeará sin darte cuenta si intentas almacenar un JWT en ella.
Lo que hace diferente a los cookies
Los cookies se envían automáticamente con cada solicitud HTTP que coincida. Esto es tanto una característica como un problema. Significa que tu cookie de sesión llega al servidor sin que intervenga JavaScript — y también significa que cada solicitud a api.example.com lleva el costo de los cookies, independientemente de si lo deseas o no.
Los atributos que realmente importan:
- impide que JavaScript lea la cookie mediante — JavaScript no puede leer esta cookie. Los ataques XSS no pueden exfiltrarla. Esto es una condición básica para las cookies de sesión.
- Seguro — solo se envía sobre HTTPS. Sin este atributo en un sitio en producción, estás enviando cookies de autenticación sobre HTTP. No lo hagas.
- SameSite=Strict — la cookie solo se envía cuando la solicitud proviene de tu propio dominio. Ofrece protección contra CSRF, pero rompe los flujos de redirección de OAuth. SameSite=Lax es un equilibrio razonable para la mayoría de las aplicaciones.
- Expires / Max-Age — sin estos, es una cookie de sesión que se borra cuando se cierra el navegador. Establece una fecha de expiración explícita para el comportamiento "recordarme".
// Setting a cookie from JavaScript (no HttpOnly, obviously)
document.cookie = "theme=dark; Path=/; Max-Age=31536000; Secure; SameSite=Lax";
// Server-side (Node.js + Express) — where the real power is
res.cookie('sessionId', token, {
httpOnly: true, // JS cannot read this
secure: true, // HTTPS only
sameSite: 'lax', // balanced CSRF protection
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days in ms
});
Si estás depurando un string de cookie complicado, IO Tools’ Cookie Parser lo descompone en pares clave-valor legibles — útil cuando estás mirando una cabecera de 400 caracteres Set-Cookie y tratando de determinar cuál atributo está mal.
Dónde falla
- Límite de 4 KB — este es el tamaño total incluyendo nombre + valor + todos los atributos. Una JWT típica mide entre 400 y 800 bytes. Una gruesa con muchos claims mide entre 1 y 2 KB. No tienes mucho espacio.
- SameSite=Strict rompe OAuth — cuando tu proveedor de identidad redirige de vuelta a tu aplicación tras el login, se trata de una solicitud de sitio cruzado. SameSite=Strict significa que tu cookie de sesión no será enviada. Usa Lax para cualquier cosa que pase por flujos de OAuth.
- Orden de cookies y coincidencia de ruta — cuando varias cookies tienen rutas superpuestas, el navegador las envía en un orden indefinido. No confíes en la prioridad.
- Deprecación de cookies de terceros — Chrome está eliminando las cookies de terceros. Las dependencias de cookies de sitio cruzado son una responsabilidad a mediano plazo.
El debate sobre almacenar JWT en localStorage (la superficie real de ataque)
Este punto surge en cada discusión sobre autenticación, y la mayoría de las personas que lo discuten están hablando por separado.
La preocupación al almacenar JWTs en localStorage: si tu sitio tiene una vulnerabilidad XSS, un script de atacante puede leer el token directamente y exfiltrarlo. El atacante ahora tiene un token de autenticación válido que puede usar desde cualquier lugar, en cualquier dispositivo, hasta que expire.
La argumentación a favor de cookies HttpOnly en lugar de ello: JavaScript no puede leer la cookie, por lo que XSS no puede exfiltrarla. La sesión sigue siendo útil en las solicitudes (el atacante puede hacer solicitudes desde el navegador del víctima mediante XSS), pero no puede robar el token para usarlo en otro lugar. Esto limita el alcance del ataque.
La opinión honesta: el problema real es XSS, no la ubicación de almacenamiento. Si tienes XSS, una cookie HttpOnly es significativamente mejor — el atacante no puede llevar el token fuera del sitio. Pero corregir XSS es un objetivo más significativo, alcanzable con una política de seguridad estricta, sin scripts de terceros no controlados y con escape adecuado de salida.
Si estás construyendo una SPA con una política de seguridad estricta y sin scripts de terceros que no controlas, localStorage probablemente está bien para JWTs. Si estás ejecutando un sitio con Google Tag Manager, píxeles de anuncios y una docena de dependencias de npm, cookies con HttpOnly son mucho más seguras porque tu superficie de ataque XSS es mayor de lo que crees.
Al depurar problemas con JWT, el JWT Decoder en IO Tools es útil — pega el token y ve el payload y la fecha de expiración sin escribir código. El Verificador de vencimiento JWT es útil para confirmar si un token sigue siendo válido cuando estás rastreando una cascada de 401.
Flujo de decisión
Antes de abrir una pestaña de Stack Overflow a las 3 de la madrugada, recorre esto:
- ¿Necesita el servidor leerlo? → Cookie. Fin de la discusión.
- ¿Es mayor de 5 MB o necesitas consultabilidad? → IndexedDB.
- ¿Debe desaparecer al cerrar la pestaña? → sessionStorage.
- Todo lo demás → localStorage, con precaución adecuada sobre lo que estás almacenando.
La única cosa que todos hacen mal
Los APIs de almacenamiento no son confiables en todos los usuarios en todo momento. Pueden ser borrados por el navegador, el sistema operativo, ajustes de privacidad o el usuario. Cualquier arquitectura que trate al almacenamiento en el cliente como fuente de verdad — en lugar de como un caché — eventualmente fallará a alguien.
El patrón que funciona: tratar el almacenamiento del navegador como una optimización de rendimiento sobre tu fuente de verdad real (el servidor). Cachear de forma agresiva, pero diseñar tu aplicación para recuperarse de forma gráfica cuando el caché esté vacío. Las sesiones de depuración a las 3 de la madrugada casi nunca son sobre qué API de almacenamiento usaste — son sobre asumir que los datos estarían allí cuando no estaban.
Instalar extensiones
Agregue herramientas IO a su navegador favorito para obtener acceso instantáneo y búsquedas más rápidas
恵 ¡El marcador ha llegado!
Marcador es una forma divertida de llevar un registro de tus juegos, todos los datos se almacenan en tu navegador. ¡Próximamente habrá más funciones!
Herramientas clave
Ver todo Los recién llegados
Ver todoActualizar: Nuestro última herramienta se agregó el 18 de junio de 2026
