wandres.dev
EL MAPA DEL ALMACENAMIENTO · cookies, local y session

Cookies y sessionStorage: el impuesto de red y el ámbito de pestaña

Por qué una cookie viaja adjunta a cada petición del ámbito y eso la descalifica como almacén de datos, y qué significa exactamente que sessionStorage viva y muera con una pestaña.

⏱ 16 min

Cookies y sessionStorage comparten un rasgo que las separa del resto del inventario: ninguna de las dos fue diseñada para guardar datos, sino para acotar un ámbito. La cookie acota un ámbito de red —un dominio, una ruta, un contexto de petición— y lo hace pagando un peaje en cada byte que sale del navegador. sessionStorage acota un ámbito de vida —una pestaña, mientras exista— y lo hace con reglas de clonación y herencia que casi nadie conoce hasta que le muerden. Entender ambos ámbitos con precisión es lo que evita los dos errores más caros de esta capa: usar cookies como base de datos y suponer que sessionStorage es simplemente localStorage con menos memoria.

🎯 Al terminar esta lección sabrás
  • Entender por qué la cookie viaja adjunta a cada petición de su ámbito y qué cuesta eso.
  • Enumerar los atributos que definen el ámbito de una cookie y el efecto de cada uno.
  • Describir con exactitud el ámbito de sessionStorage, incluida su clonación al duplicar pestaña.
  • Delimitar el único trabajo que cada una hace mejor que cualquier otra alternativa.

Empecemos por deshacer la palabra. Llamar almacenamiento a una cookie es ya un error de categoría, y de ese error se derivan casi todos los abusos que se cometen con ella.

Una cookie no es un lugar donde se guarda algo: es una instrucción permanente al navegador para que adjunte ese valor a la cabecera Cookie de toda petición que caiga dentro de su ámbito. Y toda significa toda: el documento HTML, cada hoja de estilos, cada script, cada imagen, cada fuente, cada llamada a la API. Si un origen tiene cuatro kilobytes de cookies —el mínimo que la RFC 6265 exige que un agente de usuario soporte por cookie— y una página carga cien subrecursos de ese mismo origen, el navegador ha enviado cuatrocientos kilobytes de cabeceras que nadie pidió.

Ese cálculo, además, se repite en cada visita: la cookie no se envía una vez y se cachea, se adjunta siempre, porque su valor puede haber cambiado y el servidor necesita el actual. Es un coste recurrente, no una inversión inicial.

Ese coste tiene tres agravantes que lo hacen peor de lo que parece. Va en subida, la dirección donde el ancho de banda doméstico y móvil es más escaso. Va en las cabeceras, que se envían antes que el cuerpo y por tanto retrasan el inicio de todo lo demás. Y es invisible en las métricas habituales, que miden el peso de la respuesta y casi nunca el de la petición.

flowchart LR
N[Navegador] -->|documento mas cookie| S[Servidor]
N -->|hoja de estilos mas cookie| S
N -->|script mas cookie| S
N -->|imagen mas cookie| S
N -->|llamada a la API mas cookie| S
S --> R[el coste se paga en cada flecha]
style R fill:#f38ba8,color:#11111b

Hay además un detalle que amplifica el peaje y que suele ignorarse: el ámbito de una cookie no coincide con el origen. Las cookies se rigen por dominio y ruta, no por la terna de esquema, host y puerto que define el origen de Web Storage. Una cookie fijada con Domain en el dominio raíz acompaña también a las peticiones de todos sus subdominios, incluidos los que sirven imágenes, fuentes o recursos estáticos y que no necesitan saber absolutamente nada de la sesión del usuario. De ahí la práctica, ya clásica, de servir los estáticos desde un dominio distinto: no es una optimización de caché, es una amputación deliberada del ámbito de las cookies.

El ámbito lo definen unos pocos atributos, y cada uno mueve la frontera en una dirección distinta. Domain amplía el alcance a los subdominios; Path lo estrecha a una rama de la jerarquía de rutas; Expires y Max-Age deciden si la cookie sobrevive al cierre del navegador o muere con la sesión; Secure la restringe a conexiones cifradas; HttpOnly la vuelve invisible para JavaScript; SameSite gobierna si acompaña a peticiones originadas desde otro sitio; y el atributo de particionado, en los navegadores que lo implementan, aísla las cookies de terceros por sitio de nivel superior.

// La API del cliente es una unica cadena que hay que analizar a mano
document.cookie = "tema=oscuro; Max-Age=31536000; Path=/; SameSite=Lax; Secure";

// Leer implica trocear: no hay estructura, solo texto
const cookies = Object.fromEntries(
  document.cookie.split("; ").map((par) => par.split("="))
);
cookies.tema; // "oscuro"
⚠️
HttpOnly es la razón por la que la cookie sigue siendo insustituible

Una cookie marcada HttpOnly no es accesible desde document.cookie, lo que significa que un script inyectado en tu página no puede leerla. Ningún mecanismo de Web Storage ofrece esa propiedad: localStorage y sessionStorage son legibles por cualquier script que se ejecute en el origen, sin excepción. Por eso el token de sesión pertenece a una cookie HttpOnly y Secure, y no a Web Storage, por incómodo que resulte.

El peaje de red bastaría para descartarla como almacén general, pero conviene enumerar el resto, porque son razones estructurales y no accidentes de implementación que alguien pudiera arreglar en una versión futura. No tiene tipos: es una cadena, y el cliente ni siquiera recibe un mapa, sino una única cadena con todas las cookies concatenadas que hay que trocear a mano. No tiene transacciones ni atomicidad: escribir varias cookies son varias operaciones independientes. Y no tiene control de expulsión: cuando se supera el número de cookies permitido por dominio, el navegador expulsa según sus propias reglas, no según tu prioridad.

La API del cliente merece un comentario aparte, porque es probablemente la peor de toda la plataforma. document.cookie es una propiedad que al leerse devuelve todas las cookies concatenadas en una sola cadena y al asignarse añade o modifica una, con una asimetría entre lectura y escritura que no tiene equivalente en ninguna otra API del navegador. No hay forma de borrar directamente: se borra fijando una fecha de caducidad en el pasado. No hay forma de enumerar atributos: al leer solo obtienes nombres y valores, nunca Path, Domain ni SameSite. Existe una API moderna de cookies, asíncrona y basada en promesas, que corrige buena parte de esto, pero su disponibilidad todavía es desigual entre navegadores y conviene comprobarla antes de depender de ella.

A esto se añade una asimetría que a menudo se olvida: la cookie es el único mecanismo del inventario que el servidor puede escribir y leer directamente. Esa es su razón de ser y también su límite. Todo lo que pongas en ella lo verá el servidor quiera o no, lo cual convierte cualquier dato de aplicación guardado ahí en una fuga de información involuntaria y en un coste permanente.

Y hay un límite de cardinalidad, además del de tamaño: la RFC 6265 fija un mínimo de cookies por dominio que los agentes deben soportar, pero no un máximo garantizado, de modo que al superar el número que cada navegador admita el propio navegador decide cuál expulsar. No tu código, no tu prioridad: su heurística. Un almacén del que una autoridad externa puede borrar entradas según criterios que no publicas ni controlas no es un almacén sobre el que se pueda razonar.

// Borrar una cookie es fijarle una caducidad en el pasado
document.cookie = "tema=; Max-Age=0; Path=/";

// La lectura no devuelve atributos: Path, Domain y SameSite son invisibles
document.cookie; // "sesion=abc; tema=oscuro" y nada mas
🚚

Coste por petición

Se envía en cada petición del ámbito, en subida y en las cabeceras. El precio se paga tantas veces como recursos cargue la página.

🧵

Sin estructura

Una sola cadena para todas las cookies, sin tipos, sin anidamiento y con reglas de codificación que hay que aplicar a mano.

👁️

El servidor lo ve todo

Cualquier dato guardado ahí viaja al servidor por diseño. Lo que en otros mecanismos es privado del cliente, aquí es público para el backend.

🛡️

Su ventaja real es la seguridad

HttpOnly, Secure y SameSite dan garantías que ningún otro almacén del navegador puede ofrecer a un token de sesión.

sessionStorage: una pestaña, un mundo

Del lado del cliente, sessionStorage es el mecanismo peor comprendido del inventario, y no por complejidad sino por una analogía engañosa: su nombre sugiere sesión en el sentido de sesión de usuario, cuando lo que acota es una sesión de navegación. Las dos cosas se parecen lo justo para confundirlas y difieren exactamente donde importa.

sessionStorage expone exactamente la misma interfaz que localStorage —síncrona, de cadena a cadena, con el mismo orden de magnitud de cuota— y difiere en una sola cosa: su ámbito. Los datos pertenecen al contexto de navegación de nivel superior, es decir, a la pestaña. Sobreviven a las recargas y a la navegación dentro del mismo origen, y desaparecen cuando la pestaña se cierra.

Las reglas finas son las que sorprenden, y conviene enunciarlas sin ambigüedad:

  • Dos pestañas con la misma URL no comparten nada. Cada una tiene su propia área, aislada de la otra. Esto es lo que la hace útil para flujos que el usuario podría querer ejecutar dos veces en paralelo.
  • Duplicar una pestaña clona el área. El navegador copia el contenido a la pestaña nueva en el momento de la duplicación; a partir de ahí las dos evolucionan por separado. No es un enlace, es una copia.
  • Abrir una ventana desde la página hereda una copia cuando el nuevo contexto se crea a partir del actual y comparte origen.
  • Los iframe del mismo origen dentro de la pestaña comparten el área. El ámbito es la pestaña, no el documento.
  • Restaurar una sesión suele restaurar el área, porque el navegador considera que la pestaña continúa y no que nace de nuevo.

Esa lista tiene una lectura conceptual que la hace fácil de recordar: el ámbito de sessionStorage no es la pestaña en sentido físico, sino el contexto de navegación de nivel superior, y ese contexto se hereda por clonación cuando nace de otro y se destruye cuando se cierra. Todo lo demás —los iframe que comparten, las pestañas gemelas que no, la copia al duplicar— se deduce de esa única definición sin necesidad de memorizar casos.

Conviene también nombrar la comparación que casi nadie hace: sessionStorage comparte con localStorage todos sus defectos. Es igual de síncrono, bloquea el mismo hilo, guarda cadenas y solo cadenas, carece de transacciones y avisa de la cuota agotada con la misma excepción tardía. Elegirlo sobre localStorage nunca debe justificarse por rendimiento ni por capacidad, porque en ambos ejes son idénticos: la única razón legítima es que el dato deba morir con la pestaña.

// El mismo API que localStorage, otro ambito de vida
sessionStorage.setItem("pasoDelAsistente", "3");

// Un flujo que el usuario puede tener abierto dos veces sin interferencia
sessionStorage.setItem("idBorradorEnCurso", crypto.randomUUID());
ℹ️
La clonación al duplicar es una fuente clásica de identificadores repetidos

Si guardas en sessionStorage un identificador que asumes único por pestaña, duplicar la pestaña te deja dos contextos con el mismo valor, y a partir de ese instante ambos creerán ser el mismo flujo. El síntoma llega tarde y es difícil de reproducir. La defensa habitual es no confiar en la unicidad del valor clonado y regenerarlo cuando el contexto detecta que no le pertenece.

Lo que cada una hace bien

Reducidas a su función esencial, ninguna de las dos es un almacén general y ambas son insustituibles en su nicho, lo cual es una combinación poco habitual y que explica su longevidad. La cookie es el mecanismo correcto cuando el servidor necesita ver el valor en cada petición y cuando el valor debe estar fuera del alcance de los scripts de la página: identidad de sesión, protección contra falsificación de peticiones, enrutado a una región. sessionStorage es el mecanismo correcto cuando el dato debe morir con la pestaña y no contaminar a las hermanas: el paso de un asistente, un carrito de compra que no debe mezclarse entre pestañas, el estado de un formulario largo que sobrevive a una recarga accidental.

Fuera de esos dos nichos, ambas son la respuesta equivocada, y el error se paga con moneda distinta en cada caso: con la cookie se paga en ancho de banda de subida en cada petición durante toda la vida del producto; con sessionStorage se paga de golpe, el día que un usuario pierde media hora de trabajo porque cerró la pestaña que era, sin él saberlo, el único lugar donde existía.

Necesidad Mecanismo Motivo
Identidad de sesión Cookie HttpOnly | Secure El servidor la exige y el script no debe verla
Preferencia visible antes de pintar localStorage Persiste y se lee de forma síncrona
Paso de un asistente por pestaña sessionStorage Aislado entre pestañas y efímero por diseño
Datos de aplicación Ninguno de los tres Sin transacciones, sin cuota suficiente
📝
Un flujo que el usuario abre dos veces es el caso que solo sessionStorage resuelve

Piensa en un usuario que abre dos pestañas para comparar dos reservas, dos configuraciones de producto o dos formularios largos. Con localStorage, ambas pestañas escriben sobre las mismas claves y la última en escribir corrompe el estado de la otra sin que nadie se entere; con IndexedDB tendrías que inventar tú un identificador de sesión por pestaña y arrastrarlo por toda la aplicación. sessionStorage te da ese aislamiento gratis y por construcción, y ese es exactamente el problema para el que fue diseñado.

Ambas son ámbitos disfrazados de almacenes, y el ámbito es lo que estás eligiendo

La forma más productiva de pensar estos dos mecanismos es dejar de verlos como sitios donde caben bytes y verlos como declaraciones sobre quién puede ver un dato y durante cuánto tiempo; el almacenamiento es, en ambos casos, el efecto secundario de esa declaración, no su propósito. Cuando escribes una cookie no estás guardando: estás firmando un contrato con el navegador según el cual ese valor acompañará a cada petición que caiga dentro de un dominio, una ruta y una política de origen cruzado, hasta una fecha determinada, visible u oculto a los scripts según lo hayas marcado. Ese contrato es extraordinariamente expresivo —HttpOnly y SameSite codifican garantías de seguridad que ninguna otra API del navegador sabe expresar— y por eso la cookie sobrevive a treinta años de sustitutos que iban a jubilarla; pero es también un contrato caro, porque su unidad de cobro es la petición y las aplicaciones modernas hacen muchísimas. Cuando escribes en sessionStorage, análogamente, no estás eligiendo un almacén pequeño: estás declarando que ese dato pertenece a una ventana concreta de la atención del usuario y que no tiene sentido fuera de ella, lo cual es una afirmación semántica fuerte y sorprendentemente útil, porque resuelve de raíz el problema de las pestañas paralelas que a localStorage le resulta imposible ni siquiera plantear. De ahí se sigue el criterio que hay que interiorizar y que se generaliza a todo el resto del track: antes de preguntarte cuánto ocupa un dato, pregúntate quién debe verlo y cuándo debe dejar de existir, porque esas dos respuestas eliminan la mayoría de las opciones antes de que el tamaño llegue siquiera a ser relevante. Un token de sesión no está en una cookie porque sea pequeño, sino porque el servidor tiene que verlo y el script no; el paso de un asistente no está en sessionStorage porque quepa, sino porque su vida coincide con la de la pestaña. El tamaño decide entre los candidatos que sobreviven al filtro del ámbito, nunca al revés, y quien invierte ese orden termina explicando por qué su aplicación envía megabytes de cabeceras o por qué dos pestañas del mismo usuario se están pisando el estado.

⚔️ Mide el peaje y rompe el ámbito
  1. Abre las herramientas de red en una aplicación que uses y suma el tamaño de la cabecera Cookie de todas las peticiones de una carga completa. Compáralo con el peso de un recurso mediano.
  2. Escribe una cookie con Path restringido y comprueba en qué peticiones aparece y en cuáles no. Repite con Domain ampliado a un subdominio.
  3. Guarda un identificador en sessionStorage, duplica la pestaña y verifica que ambas tienen el mismo valor. Diseña una estrategia para detectar la clonación y regenerarlo.
  4. Coloca un iframe del mismo origen en tu página y comprueba que comparte el área de sessionStorage con el documento contenedor. Razona qué implica eso para el aislamiento de un widget embebido.