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

El inventario completo del almacenamiento del navegador

Cookies, localStorage, sessionStorage, IndexedDB, Cache API y OPFS: qué es cada mecanismo, cuándo apareció y para qué problema fue diseñado realmente, antes de decidir cuál usar.

⏱ 16 min

El navegador no tiene un almacenamiento: tiene seis, ninguno sustituyó al anterior y todos siguen vivos. Esa acumulación no es desorden accidental sino un registro sedimentario de treinta años de ambiciones sucesivas de la web, y leerlo como tal —qué problema resolvía cada capa el año en que se depositó— es lo que convierte una lista de APIs inconexas en un mapa utilizable. Quien elige almacenamiento sin conocer la intención original de cada mecanismo acaba, invariablemente, usando el equivocado: una cookie como base de datos, localStorage como caché de red, IndexedDB como archivo de configuración.

🎯 Al terminar esta lección sabrás
  • Enumerar los seis mecanismos de almacenamiento del navegador y su propósito de diseño.
  • Situar cada uno en su momento histórico y entender qué problema vino a resolver.
  • Distinguir los que tienen cuota fija propia de los que comparten la cuota dinámica del origen.
  • Reconocer por qué la web no elimina APIs y qué implica eso para tus decisiones.

Un inventario que es un registro sedimentario

La web casi nunca borra. Su compromiso con la compatibilidad hacia atrás significa que una API que alguna vez se usó en producción sobrevive indefinidamente, aunque exista un sucesor mejor. El resultado es que el almacenamiento del navegador se lee como una columna estratigráfica: cada estrato responde a una pregunta distinta, planteada en una década distinta, con las restricciones técnicas de aquel momento fosilizadas en la forma de su API.

Antes de recorrer los estratos conviene fijar la unidad que los organiza a todos: el origen, es decir, la terna de esquema, host y puerto. Ninguno de los seis mecanismos comparte datos entre orígenes distintos, y esa frontera no es una limitación de implementación sino el cimiento del modelo de seguridad de la web. Todo lo que sigue —cuotas, desalojo, ámbitos, permisos— se contabiliza y se decide por origen.

Los cinco estratos, en orden de deposición:

Cookies (1994). Nacieron en Netscape para dar memoria a un protocolo deliberadamente sin estado. Su propósito no era guardar datos sino transportar un identificador de sesión: el almacenamiento fue un efecto colateral de la necesidad de que el servidor viese el valor en cada petición. Toda su ergonomía —cadena única, tamaño mínimo garantizado de cuatro kilobytes por la RFC 6265, atributos de ámbito y caducidad— deriva de que su destino real es una cabecera HTTP.

Web Storage: localStorage y sessionStorage (finales de los 2000). La primera respuesta explícita a la pregunta quiero guardar una cadena sin pagar red. Un mapa de clave y valor, síncrono, de cadenas a cadenas. Su API es minúscula porque en 2009 la alternativa era una cookie y las cargas eran diminutas; nadie diseñaba pensando en megabytes.

IndexedDB (Recomendación del W3C en 2015). Llegó después del fracaso de Web SQL, abandonado en 2010 porque una especificación que dice comportaos como SQLite no es una especificación. IndexedDB es transaccional, asíncrona, indexada, guarda valores estructurados y no cadenas, y es la única base de datos que todos los navegadores traen de serie.

Cache API (mediados de los 2010). Nació con los service workers para un problema concreto: guardar pares de petición y respuesta HTTP para servirlos sin red. Su clave no es una cadena arbitraria sino un Request, y su valor un Response completo con cabeceras. No es almacenamiento de datos de aplicación: es un almacén de tráfico.

OPFS (principios de los 2020, con soporte cruzado desde 2023). El origin private file system es un sistema de archivos virtual y privado del origen, sin diálogos de permiso ni relación alguna con el disco visible del usuario. Se diseñó para dar a los tiempos de ejecución de WebAssembly —SQLite, DuckDB— la abstracción que necesitaban: archivos, desplazamientos, lecturas y escrituras parciales.

Puestas en fila, las cinco capas responden a cinco preguntas que la web se fue haciendo por orden, y cada respuesta solo tiene sentido a la luz de su pregunta:

  • ¿Cómo recuerda un protocolo sin estado quién eres entre dos peticiones? → la cookie.
  • ¿Cómo guardo una cadena sin pagar red en cada petición? → Web Storage.
  • ¿Cómo consulto datos que no caben en memoria sin bloquear la interfaz? → IndexedDB.
  • ¿Cómo sirvo una página entera sin conexión? → la Cache API.
  • ¿Cómo doy a un motor compilado a WebAssembly los archivos que espera? → OPFS.
flowchart LR
A[1994 Cookies] --> B[2009 Web Storage]
B --> C[2015 IndexedDB]
C --> D[Cache API con service workers]
D --> E[OPFS sistema de archivos del origen]
A --> F[el servidor lo ve]
E --> G[solo el cliente lo ve]

Nótese que la línea temporal y la línea de visibilidad son la misma línea recorrida en direcciones opuestas: a medida que el almacenamiento del navegador ganó capacidad, fue perdiendo público. La cookie la ve el servidor en cada petición; OPFS no la ve nadie más que el propio origen, ni siquiera el usuario a través de su explorador de archivos. Esa privatización progresiva es el prerrequisito silencioso de todo el movimiento local-first: sin un lugar donde el cliente pueda guardar mucho y en privado, la idea de que la copia local sea la verdadera no habría pasado de manifiesto.

Qué es cada uno, en una frase

Antes de las definiciones, una advertencia sobre el vocabulario. Se habla de almacenamiento local como si fuera una categoría, y no lo es: bajo ese paraguas caben mecanismos cuya vida va desde una pestaña hasta años, cuya visibilidad va desde el servidor hasta nadie salvo el origen, y cuyo tamaño va desde kilobytes hasta una fracción del disco. La única forma de no confundirlos es nombrarlos con precisión, uno por uno.

🍪

Cookies

Cadenas diminutas que el navegador adjunta automáticamente a cada petición del ámbito. Transporte de sesión y de identidad, no almacén.

🗝️

localStorage

Mapa síncrono de cadena a cadena, persistente entre sesiones, limitado a 5 MB de datos en cadenas UTF-16 por origen.

🪟

sessionStorage

La misma API que localStorage, pero con ámbito de pestaña: muere cuando la pestaña muere.

🗄️

IndexedDB

Base de datos transaccional y asíncrona de objetos con índices y cursores. Guarda valores estructurados, incluidos binarios.

📦

Cache API

Almacén de pares petición y respuesta pensado para servir recursos de red sin red. La base del offline de los service workers.

📁

OPFS

Sistema de archivos virtual privado del origen, con acceso síncrono dentro de un Worker. El sustrato de las bases de datos en WebAssembly.

De la cabecera HTTP al sistema de archivos

Visto en conjunto, el inventario describe una trayectoria muy nítida en tres movimientos simultáneos. El dato pasó de ser algo que el servidor ve a ser algo que solo el cliente ve; pasó de ser una cadena a ser un valor estructurado y luego bytes crudos; y pasó de síncrono a asíncrono y de vuelta al síncrono, pero esta vez confinado a un Worker donde bloquear es legítimo.

La cuota sigue el mismo corte. Cookies y Web Storage arrastran topes propios, pequeños y fijados hace más de una década. IndexedDB, Cache API y OPFS, en cambio, comparten un único presupuesto por origen que el navegador administra de forma dinámica: Chrome puede llegar al 80% del espacio en disco, pero los datos empiezan en modo best-effort y pueden ser desalojados si el sistema anda escaso de espacio. Pedir permanencia es una llamada explícita, y el navegador puede decir que no.

La diferencia entre un tope fijo y un presupuesto dinámico es más profunda de lo que parece. Un tope fijo se conoce de antemano y se agota con un error; un presupuesto dinámico no se puede consultar como constante, cambia con el disco del usuario y con el comportamiento de otros orígenes, y su forma de fallar no es un error sino la desaparición silenciosa de datos que creías tuyos. Programar contra el primero es aritmética; programar contra el segundo exige diseñar para la pérdida.

También cambia la forma de las claves. Web Storage y las cookies indexan por una cadena; IndexedDB por una clave que puede ser compuesta, con índices secundarios encima; la Cache API por un Request; y OPFS por una ruta dentro de una jerarquía de directorios. Esa progresión —de cadena a clave estructurada a recurso a ruta— es la misma trayectoria vista desde el otro lado: cuanto más rica es la clave, más selectivamente puedes recuperar sin materializarlo todo.

La consulta del presupuesto es una sola llamada, y merece la pena ejecutarla en el arranque de cualquier aplicación que aspire a guardar algo serio en el cliente:

// Cuanto hay y cuanto queda, en los tres mecanismos con cuota dinamica
const { usage, quota } = await navigator.storage.estimate();
console.log(usage, quota); // bytes usados y presupuesto concedido al origen

// Salir de best-effort: el navegador decide, no tu
const persistido = await navigator.storage.persist();
if (!persistido) {
  // sigues siendo desalojable bajo presion de disco: disenalo asi
}

Conviene subrayar que estos tres movimientos no son independientes: se implican. El dato dejó de viajar al servidor porque creció; creció porque dejó de ser una cadena; y dejó de ser una cadena porque el acceso parcial se volvió imprescindible. Cada eslabón fuerza al siguiente, y por eso el inventario no es una colección de opciones equivalentes sino una escalera cuyos peldaños se recorren en un orden bastante determinado a medida que una aplicación madura.

📝
El estrato en el que estás dice mucho de tu aplicación

Hay una correlación empírica que se sostiene sorprendentemente bien: las aplicaciones que solo usan cookies y Web Storage tratan al navegador como un terminal y ponen toda la verdad en el servidor; las que usan IndexedDB tratan al cliente como una caché con opinión propia; y las que llegan a OPFS han decidido que el cliente es un participante de pleno derecho, con su propio motor de datos. El estrato no es un detalle de implementación, es una declaración sobre dónde vive la autoridad.

⚠️
Best-effort significa exactamente lo que dice

Los datos de IndexedDB, Cache API y OPFS nacen en modo best-effort: el navegador los conserva mientras pueda y los desaloja si el sistema se queda sin espacio. navigator.storage.persist() solicita el modo persistente, pero es una petición, no una orden, y su concesión depende de heurísticas de compromiso del usuario con el sitio. Una arquitectura local-first que asume que sus datos locales son indestructibles está construida sobre una premisa falsa.

Lo que el inventario no incluye

Tan importante como saber qué hay es saber qué no hay, porque las ausencias determinan la arquitectura tanto como las presencias. No existe almacenamiento con garantía absoluta de permanencia: el usuario siempre puede borrarlo, y las políticas de privacidad de los navegadores han ido acortando la vida de los datos de orígenes con los que se interactúa poco. No existe almacenamiento compartido entre orígenes: cada origen vive en su propio compartimento estanco, y eso es una decisión de seguridad, no una carencia; los navegadores modernos han ido más lejos y particionan incluso el almacenamiento de terceros por sitio de nivel superior, de modo que un mismo widget embebido en dos sitios distintos ve dos almacenes distintos. No existe cifrado en reposo por defecto: lo que guardas queda legible para cualquiera con acceso al perfil del navegador, y el cifrado extremo a extremo, cuando hace falta, hay que construirlo encima con las primitivas de la Web Crypto API.

Y hay un estrato extinto que conviene conocer: Web SQL, retirado por imposible de especificar, es la única excepción real a la regla de que la web no borra. Su epitafio es la razón por la que hoy escribimos transacciones de IndexedDB en lugar de SELECT, y por la que SQLite volvió al navegador por la puerta de atrás, compilado a WebAssembly sobre OPFS.

La historia de Web SQL deja además una lección metodológica que va más allá del almacenamiento. La especificación decía, en esencia, comportaos como esta implementación concreta, y un estándar que se define por referencia a un único producto no es un estándar: es una dependencia con otro nombre. La web prefirió pasar años sin base de datos cómoda antes que consagrar esa dependencia, y hoy, con SQLite ejecutándose como WebAssembly sobre un sistema de archivos especificado, ha conseguido lo que quería —el mismo motor— sin que su especificación dependa de él.

// Cada mecanismo indexa de una forma distinta, y esa es la diferencia real
localStorage.setItem("tema", "oscuro");                   // cadena a cadena
await cache.put(new Request("/api/v1/perfil"), respuesta); // recurso a recurso
const raiz = await navigator.storage.getDirectory();       // ruta a archivo
const archivo = await raiz.getFileHandle("datos.sqlite", { create: true });

Falta además una pieza que en un sistema operativo se da por descontada: no hay un mecanismo de bloqueo integrado en el almacenamiento. Nada impide que dos pestañas del mismo origen escriban a la vez sobre la misma entrada, y ninguna de las seis APIs coordina por ti. La coordinación entre pestañas es un problema aparte, con sus propias herramientas —BroadcastChannel, la API de Web Locks, los Workers compartidos— y el track le dedica un nivel entero más adelante, precisamente porque el almacenamiento no lo resuelve.

ℹ️
Seis mecanismos, tres presupuestos distintos

Las cookies compiten por un tope propio expresado en cabecera. Web Storage tiene su propio límite de 5 MB por origen. IndexedDB, Cache API y OPFS comparten la cuota dinámica del origen. Llenar localStorage no consume cuota de IndexedDB, y viceversa: son contabilidades separadas, y confundirlas lleva a diagnósticos erróneos cuando algo empieza a fallar por falta de espacio.

El inventario es una arqueología de las ambiciones de la web

Hay una lectura del almacenamiento del navegador que lo presenta como un accidente lamentable, seis APIs incoherentes que un diseño limpio habría reducido a una; esa lectura es cómoda y equivocada, y quien la adopta pierde la única herramienta fiable para elegir bien. Cada estrato de este inventario responde con precisión quirúrgica a una pregunta que la web se hacía el año en que se depositó, y la forma de su API es la huella fosilizada de esa pregunta. La cookie es síncrona, minúscula y viaja sola porque en 1994 el problema no era guardar sino recordar entre peticiones, y el único canal disponible era la cabecera. localStorage es un mapa de cadenas bloqueante porque en 2009 el problema era librarse de la cookie para cuatro preferencias, y en un mundo de kilobytes el coste de bloquear el hilo era literalmente imperceptible. IndexedDB es tan incómoda de usar precisamente porque nació de un cadáver: tras Web SQL, el comité aprendió que especificar un lenguaje de consulta portable era inviable, y eligió deliberadamente exponer las primitivas mínimas —almacenes, índices, transacciones, cursores— dejando la ergonomía a las bibliotecas. La Cache API tiene por clave un Request porque no fue concebida para tus datos sino para tu tráfico. Y OPFS existe porque un runtime de WebAssembly no quiere un mapa de claves ni un almacén de objetos: quiere pread y pwrite, desplazamientos y bloques, y sin esa primitiva SQLite en el navegador habría sido una curiosidad de juguete en lugar de la base sobre la que se construye buena parte del local-first de 2026. Entendido así, el inventario deja de ser una lista que memorizar y se convierte en un argumento: la pregunta correcta ante un dato nuevo nunca es qué API está de moda sino a cuál de estas seis preguntas históricas se parece más la mía. Casi siempre se parece a una sola, y cuando no se parece a ninguna, ese desajuste es la señal más valiosa que vas a recibir: significa que el dato pertenece a otra capa de tu arquitectura, o que estás a punto de construir sobre la abstracción equivocada durante los próximos dos años.

⚔️ Levanta el inventario de una aplicación real
  1. Abre una aplicación web que uses a diario y recorre las herramientas de desarrollo: anota qué guarda en cookies, en Web Storage, en IndexedDB y en Cache API.
  2. Ejecuta navigator.storage.estimate() en su consola y compara el uso declarado con lo que ves en cada mecanismo. Explica la diferencia.
  3. Identifica al menos un dato que, a tu juicio, esté en el mecanismo equivocado, y argumenta a cuál pertenecería según el propósito de diseño de cada uno.
  4. Comprueba si el origen tiene almacenamiento persistente concedido y razona qué perdería el usuario si el navegador lo desalojase esta noche.