wandres.dev
CUOTAS Y DESALOJO · cuando el navegador te borra

Best-effort frente a persistente: los dos modos

Todo lo que escribe tu aplicación nace en modo best-effort, un contrato que autoriza al navegador a borrarlo sin avisar cuando el sistema anda escaso de espacio, y solo el modo persistente lo exime de ese desalojo.

⏱ 16 min

Hay una frase que casi nadie lee y que debería estar impresa en la primera página de cualquier tutorial de IndexedDB: los datos que acabas de guardar pueden desaparecer sin que nadie te avise. No por un error, no por un fallo de disco, no por una acción del usuario: por diseño. El almacenamiento del navegador distingue dos modos —best-effort y persistente— que no son dos APIs distintas ni dos configuraciones de rendimiento, sino dos contratos de permanencia radicalmente diferentes. Y el hecho incómodo, el que reordena todo lo que creías sobre tu arquitectura local-first, es que salvo que hagas algo al respecto estás siempre en el primero.

🎯 Al terminar esta lección sabrás
  • Distinguir los dos modos de almacenamiento como contratos de permanencia y no como opciones de configuración.
  • Entender qué autoriza exactamente el modo best-effort y bajo qué condiciones se ejerce esa autorización.
  • Precisar qué garantiza y qué no garantiza el modo persistente, evitando confundirlo con inmortalidad.
  • Asumir la consecuencia de que el modo por defecto es el frágil y decidir qué datos no pueden vivir ahí.

Dos contratos, no dos APIs

Lo primero que hay que desactivar es la intuición de que se trata de dos formas de guardar. No las hay: escribes en IndexedDB, en Cache Storage o en OPFS exactamente igual en ambos modos, con el mismo código y el mismo rendimiento. Lo que cambia es una propiedad del cubo de almacenamiento del origen —el recipiente único que la lección anterior identificó como unidad de contabilidad— y esa propiedad responde a una sola pregunta: si el sistema necesita espacio, ¿está el navegador autorizado a vaciar este cubo?

En modo best-effort, la respuesta es sí. El nombre es una descripción honesta del compromiso: el navegador hará cuanto pueda por conservar tus datos, y esa es toda la promesa. En modo persistente, la respuesta es no: el cubo queda exento del desalojo automático, y solo se borra por una acción deliberada, típicamente del usuario. Fíjate en que la distinción se aplica al origen entero y no a registros concretos: no existe la fila persistente dentro de una base best-effort. O se salva todo, o se pierde todo.

La distinción tampoco es una peculiaridad de IndexedDB. El estándar de almacenamiento unificó bajo un mismo cubo las APIs que antes tenían cada una sus reglas, y con ellas la política de permanencia: lo que decidas sobre el modo vale igual para tus bases de datos, para las cachés de recursos y para los ficheros de OPFS. Esa unificación simplifica el razonamiento —hay un solo contrato que entender— al precio de que no puedas elegir uno distinto para cada cosa.

// La misma escritura, el mismo codigo, en ambos modos
const tx = db.transaction("notas", "readwrite");
await tx.objectStore("notas").put(nota);

// Lo unico que cambia es una propiedad del cubo del origen
const esPersistente = await navigator.storage.persisted();
console.log(esPersistente ? "exento de desalojo" : "best-effort");

Ese cubo es único para todos los contextos del origen. La pestaña que el usuario tiene delante, las otras que dejó abiertas, los workers que lanzaste y el service worker que sirve la aplicación sin conexión escriben todos en el mismo recipiente y comparten su modo. No existe la posibilidad de aislar una parte crítica de los datos en un compartimento protegido mientras el resto queda expuesto: la granularidad del contrato es el origen, y esa granularidad gruesa es exactamente lo que obliga a razonar sobre permanencia a nivel de arquitectura y no de registro.

Qué autoriza exactamente el modo best-effort

Conviene ser preciso, porque el rumor exagera en las dos direcciones. El modo best-effort no significa que el navegador borre tus datos por capricho, ni cada semana, ni cuando la pestaña se cierra. Significa una cosa concreta: si el sistema anda escaso de espacio, el navegador puede recuperar el que ocupan los orígenes en best-effort, y lo hará sin pedir permiso ni emitir un evento que puedas escuchar. El disparador es la presión de almacenamiento, un estado del dispositivo entero sobre el que tu aplicación no tiene ninguna influencia.

flowchart TD
W[la aplicacion escribe datos] --> B[cubo del origen en modo best effort]
B --> P[el sistema anda escaso de espacio]
P --> E[el navegador desaloja el origen completo]
B --> S[solicitud de persistencia concedida]
S --> M[cubo persistente exento del desalojo automatico]
style B fill:#f9e2af,color:#11111b
style E fill:#f38ba8,color:#11111b
style M fill:#a6e3a1,color:#11111b

Merece la pena detenerse en la naturaleza del disparador, porque es la clave de por qué esto no se puede prever. La presión de almacenamiento no la genera tu aplicación: la genera el conjunto del dispositivo —el sistema operativo, las descargas del usuario, otras aplicaciones, otros orígenes— y se evalúa fuera de tu proceso. Los criterios exactos con que un navegador elige a qué orígenes desalojar primero cuando llega ese momento no están publicados con detalle y no conviene construir sobre suposiciones al respecto. Lo único que puedes dar por cierto es la autorización misma: estando en best-effort, el borrado es un desenlace legítimo.

El detalle que más gente pasa por alto es que el desalojo es atómico respecto del origen. No se libera el 30% de tus datos ni se eliminan los registros más antiguos: se vacía el cubo. Tu aplicación no se despierta con una base de datos incompleta y a medias, se despierta con una base de datos vacía, indistinguible de una primera visita. Esa característica, que a primera vista parece más brutal, resulta ser una bendición de diseño: no tienes que programar la reconciliación de un estado parcialmente amputado, sino un arranque en frío correcto. La lección cinco de este nivel convierte esa observación en una arquitectura.

ℹ️
Sin evento, sin aviso, sin oportunidad de reaccionar

No existe un onbeforeevict al que suscribirse. El desalojo puede ocurrir con tu pestaña cerrada, con el navegador cerrado o con el equipo en reposo, y tu código no se ejecuta durante el proceso. Esto descarta de raíz toda una familia de soluciones tentadoras: no puedes exportar de urgencia al recibir un aviso, ni subir al servidor lo pendiente en el último momento, ni siquiera dejar una marca que distinga un desalojo de una instalación nueva, porque esa marca vivía en el mismo cubo que se ha vaciado. Cualquier plan que dependa de reaccionar al desalojo es, literalmente, inejecutable. Solo sirve lo que hayas hecho antes.

Tampoco conviene consolarse pensando que se trata de un supuesto teórico. La presión de almacenamiento es un estado cotidiano en una parte enorme del parque de dispositivos: portátiles con el disco pequeño y lleno, equipos compartidos, teléfonos de gama media que llevan años acumulando fotografías. El perfil de máquina en el que un desalojo es plausible no es una rareza de laboratorio; es, con toda probabilidad, el de una fracción significativa de tus usuarios, y precisamente la fracción que menos margen tiene para volver a descargarlo todo.

Qué garantiza el modo persistente y qué no

La palabra persistente invita a un exceso de confianza que conviene cortar de inmediato. Conceder la persistencia exime al cubo del desalojo automático por presión de espacio, y eso es exactamente lo que garantiza: ni un ápice más. El usuario sigue pudiendo borrar los datos del sitio desde la interfaz del navegador, desinstalar la aplicación, usar un perfil temporal o cambiar de dispositivo. El disco sigue pudiendo fallar. Y la cuota sigue existiendo, porque persistencia y tamaño son dimensiones independientes: ser persistente no te concede más espacio, solo te protege del barrido.

🛡️

Lo que sí cubre

El desalojo automático por presión de espacio, que es la única causa de pérdida sobre la que tu código no podía hacer absolutamente nada.

🧹

Lo que no cubre

El borrado deliberado del usuario desde los ajustes del navegador, que es un derecho suyo y no un fallo que debas intentar impedir.

👤

Tampoco cubre el dispositivo

Un perfil nuevo, otro navegador, otro ordenador o un disco averiado dejan tus datos fuera de alcance, sea cual sea el modo del cubo original.

📐

No es más espacio

Persistencia y cuota son ejes ortogonales: te protege del desalojo, no del QuotaExceededError, que sigue esperándote cuando el presupuesto se agote.

La ortogonalidad entre permanencia y tamaño se ve mejor escrita que explicada. Son dos preguntas independientes que se responden con dos llamadas distintas, y las cuatro combinaciones posibles existen y significan cosas diferentes: un origen persistente puede estar a punto de agotar su presupuesto, y uno en best-effort puede tener espacio de sobra y aun así ser desalojado esta misma noche.

const [protegido, { usage, quota }] = await Promise.all([
  navigator.storage.persisted(),   // permanencia: me pueden borrar?
  navigator.storage.estimate(),    // tamano: me queda sitio?
]);

// Cuatro estados posibles, y ninguno implica al otro
console.log({ protegido, ocupacion: usage / quota });
⚠️
Persistente no significa que puedas dejar de sincronizar

El razonamiento peligroso empieza en cuanto llega la respuesta afirmativa: si el navegador ya no va a borrar nada, quizá la sincronización pueda relajarse o incluso posponerse a un momento más cómodo. Es un error de categoría. La persistencia protege este dispositivo de una causa concreta de pérdida; la sincronización protege al usuario de haber elegido este dispositivo. Un portátil robado, un disco averiado, un perfil que se corrompe o simplemente el teléfono que la persona lleva encima cuando quiere seguir trabajando dejan a la copia persistente exactamente igual de inalcanzable que si la hubieran desalojado. Ganar la exención es una buena noticia sobre un riesgo, no sobre todos.

Hay además un matiz de honestidad hacia el usuario que conviene no perder de vista. La persistencia no oculta tus datos ni los sustrae a su control: el navegador sigue mostrando el espacio que ocupa tu sitio en sus ajustes y sigue ofreciendo el botón que lo borra todo. Eso está bien y no debe intentar sortearse. Un origen persistente ha obtenido una exención del barrido automático, no un derecho de propiedad sobre un trozo del disco ajeno, y cualquier diseño que dependa de que el usuario no pueda borrar sus propios datos está construido sobre una premisa que la plataforma no comparte.

Vista así, la persistencia deja de parecer un interruptor mágico y adopta su papel real: elimina una causa de pérdida, la única que era completamente ajena a tu control y a la voluntad del usuario. Es una mejora sustancial del perfil de riesgo, no una garantía de durabilidad. Un ingeniero que tras conceder la persistencia deja de sincronizar con el servidor ha cambiado un riesgo alto por uno moderado y ha concluido, erróneamente, que lo ha eliminado.

Ordenar mentalmente las causas de pérdida por su relación con tu código aclara mucho el panorama. Del borrado deliberado del usuario no debes defenderte, porque es su derecho y tu trabajo es que no lo lamente. Del cambio de dispositivo te defiendes sincronizando, que es lo que harías igualmente para dar continuidad entre pantallas. Del fallo de disco te defiende cualquier réplica externa. Queda una sola causa que no tiene contrapartida útil y ante la que el usuario no ha decidido nada: el desalojo automático. Ese es, exactamente, el hueco que la persistencia tapa, y por eso vale la pena pedirla aunque no lo resuelva todo.

El hecho incómodo: por defecto estás en el primero

Y llegamos al punto que reordena la arquitectura. El modo por defecto de cualquier origen es best-effort. No hay que hacer nada para estar en él; hay que hacer algo, y con éxito incierto, para salir. Esto significa que la inmensa mayoría de las aplicaciones web que guardan datos localmente —incluida, con casi total seguridad, la tuya mientras lees esto— operan bajo un contrato que autoriza al navegador a borrarlo todo.

// El estado por defecto, comprobado antes de pedir nada
const modo = await navigator.storage.persisted(); // false en un origen nuevo

if (!modo) {
  // No es una anomalia: es el punto de partida de todo el mundo.
  // Marca los datos que no toleran desaparecer y decide su plan.
  await asegurarQueLoImportanteEstaSincronizadoOExportable();
}

Esto explica un patrón que se repite en las demostraciones de tecnología local-first: aplicaciones impecables en la sincronización, con resolución de conflictos elegante y latencia nula, que jamás mencionan en qué modo está su cubo. No es negligencia individual, es un sesgo del oficio. El desalojo no aparece en el desarrollo, no aparece en la demostración, no aparece en las pruebas automatizadas y no aparece en la primera semana de producción. Aparece meses después, disperso entre unos pocos usuarios que informan de algo tan improbable que suele archivarse como error de usuario.

Conviene sacar de aquí una consecuencia operativa concreta para el desarrollo. Como el modo por defecto es el frágil y tu máquina de trabajo probablemente no lo esté, la configuración en la que programas no es representativa de la de tus usuarios: llevas meses abriendo tu propio sitio a diario, con el disco medio vacío, y jamás verás un desalojo. La única forma de que el equipo interiorice el problema es hacerlo visible, y para eso basta con un indicador en el modo de desarrollo que muestre en todo momento el modo del cubo y el tiempo que lleva sin sincronizar el trabajo pendiente.

// En desarrollo, hazlo visible: lo que no se ve no se diseña
if (import.meta.env.DEV) {
  const modo = (await navigator.storage.persisted()) ? "PERSISTENTE" : "BEST-EFFORT";
  document.body.dataset.almacenamiento = modo;
  console.warn("Modo de almacenamiento:", modo);
}
📝
Que sea el defecto no es un descuido de la plataforma

Podría parecer un fallo de diseño de la web que lo frágil sea lo predeterminado, pero es una decisión deliberada y defendible. Si cualquier sitio pudiera reclamar almacenamiento inamovible con solo escribir en él, el disco del usuario se convertiría en un vertedero acumulado por cada página visitada una vez, y recuperar espacio exigiría una auditoría manual imposible. Al hacer el desalojo la norma y la persistencia la excepción negociada, la plataforma protege al usuario del sitio y traslada al desarrollador la carga de justificar por qué sus datos merecen quedarse. Es la misma lógica que gobierna los permisos de cámara o notificaciones: el defecto favorece a quien no eligió nada.

Best-effort no describe el almacenamiento: describe tu contrato con el usuario

La trampa conceptual de este par de modos es leerlos como una característica técnica del navegador cuando son, en realidad, una declaración sobre la promesa que tu aplicación puede hacerle legítimamente a quien la usa. Piénsalo desde el otro lado de la pantalla: cuando alguien escribe una nota en tu editor local-first y cierra la pestaña, no está entendiendo que ha realizado una escritura best-effort en un cubo desalojable; está entendiendo que ha guardado una nota. La distancia entre esas dos comprensiones es exactamente la deuda que contraes, y no la salda ningún código elegante ni ninguna base de datos rápida. Aquí es donde el ideal de longevidad del artículo fundacional de local-first —que tus datos duren más que la aplicación que los creó— deja de ser una aspiración filosófica y se vuelve una restricción de ingeniería verificable: por defecto, tus datos no duran más que la próxima vez que al usuario se le llene el disco. Y observa la ironía de la situación, porque es instructiva: la arquitectura local-first nace de desconfiar del servidor, de negarse a que la verdad viva en una máquina ajena que puede apagarse; pero al trasladar la verdad al cliente descubres que el cliente, sin persistencia concedida, ofrece una garantía de durabilidad inferior a la del servidor que acabas de despedir. Esa paradoja no invalida el enfoque, lo delimita. Lo que separa una aplicación local-first seria de una demo bonita no es qué CRDT usa ni con qué rapidez sincroniza: es haber respondido, con código y no con buenas intenciones, a la pregunta de qué le queda al usuario cuando el cubo se vacía.

⚔️ Determina el contrato real de tus datos
  1. Comprueba con navigator.storage.persisted en qué modo está hoy tu aplicación, y anota si el resultado te sorprende.
  2. Enumera cada dato que tu aplicación guarda localmente y clasifícalo en dos columnas: el que puedes volver a obtener de algún sitio y el que solo existe en ese cubo.
  3. Para cada elemento de la segunda columna, escribe la frase que le dirías al usuario si desapareciera mañana; si no puedes escribirla sin sonrojarte, ese dato no puede vivir en best-effort.
  4. Simula el desalojo borrando por completo el almacenamiento del sitio y recarga: observa si tu aplicación arranca limpiamente o si explota buscando algo que ya no está.
  5. Verifica que ninguna parte de tu plan de recuperación depende de una marca o un registro que viviera dentro del mismo cubo desalojado.