---
id: 23
title: "Tres clases de almacenamiento que no conviene confundir: Memory, Storage y Transient Storage"
slug: 0-5-three-kinds-of-storage
date: 2026/09/22
summary: "Los mismos 32 bytes de datos guardados en memory, storage o transient storage pueden costar dos órdenes de magnitud en gas, y su ciclo de vida es por completo distinto. Este artículo separa las tres clases de almacenamiento según tres ejes —propiedad, vida útil y tarificación— y explica dónde debe ir cada cosa: un bloqueo de reentrada, un array temporal o una variable de estado."
keywords: memoria de la EVM,almacenamiento de la EVM,almacenamiento transitorio,EIP-1153,SSTORE
heroImage: /images/articles/photos/0-5-three-kinds-of-storage.jpg
---

Un bloqueo de reentrada escrito en storage paga, la primera vez que un slot frío pasa de 0 a 1, 2100 de recargo por acceso frío más 20 000 de Gsset; si dentro de la misma transacción se vuelve a escribir 0, paga otros 100, con un consumo bruto de unos 22 200 gas. Esa escritura genera a la vez un reembolso de 19 900 gas, y el tope de reembolso es 1/5 del consumo total de la transacción. Con una variable de almacenamiento transitorio, las dos escrituras cuestan 100 gas cada una y no dependen del contador de reembolsos. La función es idéntica; el costo bruto difiere en unos dos órdenes de magnitud.

En la EVM hay tres zonas de datos escribibles: memory (memoria), storage (almacenamiento persistente) y transient storage (almacenamiento transitorio, EIP-1153). Las tres leen y escriben palabras de 32 bytes y las tres pueden recibir asignaciones dentro de un contrato, pero su unidad de propiedad, su tiempo de vida y sus reglas de tarificación no tienen nada que ver entre sí. Colocarlas mal no suele dar error: solo hace que el estado desaparezca cuando no se lo espera o que la factura de gas salga diez veces más alta. La pregunta que hay que responder es a quién pertenece cada clase de almacenamiento, cuánto vive y con qué reglas se tarifica, y qué conviene guardar en cada una.

## Ciclo de vida: marco, cuenta y transacción son tres escalas distintas

Memory pertenece al marco de ejecución (execution frame). El contexto al que se entra con un CALL, DELEGATECALL, STATICCALL o CREATE es un marco: al entrar, la memoria arranca a cero, y cuando el marco retorna o se revierte, se descarta entera. La memoria del marco padre no la ve el marco hijo, y la del marco hijo no vuelve al padre; para pasar datos entre dos marcos solo hay calldata y returndata.

Storage pertenece a la cuenta. Cuelga de ella como un mapeo de slots de 256 bits a valores de 256 bits; lo que se escribe entra en el trie de almacenamiento de la cuenta y pasa a formar parte del estado global, y persiste a lo largo de transacciones y bloques. Los valores cero no se escriben en el trie, así que poner un slot a cero elimina el nodo correspondiente, algo que determina directamente el diseño de las reglas de reembolso.

Transient storage pertenece también a la cuenta, pero su ámbito es una transacción. Dentro de la misma transacción, todos los marcos de esa cuenta comparten el mismo almacenamiento transitorio: el valor que escribe una llamada interna lo puede leer la llamada externa. Cuando un marco se revierte, las escrituras de ese marco se revierten con él, igual que en storage; en cambio, cuando el marco retorna con normalidad no se revierte, lo contrario que en memory. Al terminar la transacción, todo el almacenamiento transitorio se pone a cero sin condiciones. Hay una excepción más en la regla de propiedad: el almacenamiento transitorio de DELEGATECALL y CALLCODE pertenece a quien llama (el contrato que emite la instrucción), mientras que el de CALL y STATICCALL pertenece al llamado.

Las tres escalas se pueden recordar así: la memoria se cuenta por marcos, el storage por cuenta más permanencia y el almacenamiento transitorio por cuenta más transacción.

| Dimensión | Memory | Storage | Transient Storage |
|------|--------|---------|-------------------|
| Propiedad | Marco de ejecución | Cuenta | Cuenta |
| Tiempo de vida | Se crea al entrar el marco y se descarta al terminar | Persistente, se escribe en el estado global | Se pone a cero al terminar la transacción |
| Direccionamiento | Dirección de byte, se amplía por palabras de 32 bytes | Slots de 256 bits | Slots de 256 bits |
| Entre llamadas internas | No se comparte | Se comparte | La comparten todos los marcos de la misma cuenta |
| Reversión del marco | Se descarta con el marco | Revierte las escrituras de ese marco | Revierte las escrituras de ese marco |
| Costo de una escritura | 3 gas más la tarifa de ampliación | 100 a 20 000 más la tarifa de acceso frío | 100 gas fijos |

## Memory: se amplía por palabras y el costo de ampliación es cuadrático

Memory se direcciona por bytes, pero se asigna con granularidad de palabra de 32 bytes. Acceder a una palabra que aún no se ha tocado dispara una ampliación; la tarifa de ampliación se calcula sobre el total ocupado con la fórmula C_mem(a) = 3a + ⌊a² / 512⌋, donde a es el número de palabras, y el cobro real es el C_mem de después de ampliar menos el C_mem de antes. MSIZE solo crece, y dentro de un marco no se puede liberar memoria ya asignada.

La primera mitad de esa expresión es un término lineal y la segunda, un término cuadrático. Cuando a < 23 (es decir, 704 bytes), el término cuadrático se trunca a 0 y el costo parece moderado; pasado ese punto, el crecimiento se acelera. Los dos valores absolutos siguientes se calculan según la fórmula (328) del Yellow Paper, suponiendo que un único marco amplía de una vez desde memoria vacía y sin contar el costo base de MLOAD ni MSTORE: 32 KB de memoria corresponden a a = 1024 y a una tarifa de ampliación de 3 × 1024 + 1024² / 512 = 5120 gas; 1 MB de memoria corresponde a a = 32768 y a una tarifa de ampliación de 98 304 + 2 097 152 ≈ 2,19 millones de gas. Un solo marco puede consumir millones de gas solo ampliando memoria, y eso suele convertirse en la restricción dura a la hora de reservar búferes grandes dentro de un marco.

MLOAD y MSTORE tienen un costo base de 3 gas (Gverylow), y la tarifa de ampliación se suma aparte. Leer memoria que nunca se ha escrito devuelve 0, pero aun así hay que pagar la ampliación por la palabra recién tocada, porque la asignación ya ha ocurrido. Por eso las escrituras dispersas en direcciones altas son tan caras: si solo se escribe un valor en un desplazamiento muy lejano, todas las palabras intermedias se cobran como asignadas.

Los arrays temporales, los búferes de codificación y decodificación de ABI y las entradas de las funciones hash van en memory. Las variables memory, los arrays memory y los structs memory de Solidity caen en esta zona. El código siguiente asigna la longitud de una vez para no tocar palabras nuevas en cada vuelta del bucle:

```solidity
// Se amplía de una vez a n palabras; las escrituras posteriores ya no generan tarifa de ampliación
uint256[] memory buf = new uint256[](n);
for (uint256 i = 0; i < n; ++i) {
    buf[i] = i;
}
```

El límite es claro: lo que memory entrega a un marco hijo a través de CALL es una copia del contenido, y el puntero en sí solo es válido dentro del marco actual. Poner en memory un valor intermedio que deba compartirse entre marcos equivale a suponer que el marco hijo verá la memoria del padre, y esa suposición es falsa.

En ingeniería real es más frecuente optar por esquivar memory. Los bloques grandes de datos se pasan con calldata y se devuelven con returndata, se tarifan por byte y no ocupan la memoria del propio marco; solo cuando hace falta leerlos y escribirlos repetidamente dentro del marco, o construir una entrada de hash, tiene sentido desplegarlos en memoria. Ese compromiso explica por qué muchos contratos dejan el cálculo en un script externo o en una subllamada y luego reúnen los resultados por valor de retorno, manteniendo pequeño el búfer de cada marco.

## Storage: se escribe en el trie de almacenamiento de la cuenta, y escribir cuesta un orden de magnitud más que leer

En el estado global, cada cuenta tiene colgado un trie de almacenamiento cuyas claves son de 256 bits y cuyos valores son palabras de 256 bits; los valores cero no entran en el árbol. La lectura va por SLOAD. Según el mecanismo de acceso frío y caliente que introdujo EIP-2929 (actualización Berlin, bloque 12 244 000, 15 de abril de 2021), el primer acceso a un par (dirección, slot) es un acceso frío y cuesta 2100 gas (Gcoldsload); si ya se ha accedido a él en esta transacción, es un acceso caliente y cuesta 100 gas (Gwarmaccess). El conjunto de accesos fríos y calientes tiene el ámbito de la transacción: cuando el ámbito se revierte, el conjunto también se revierte.

La escritura va por SSTORE, y su costo lo deciden las reglas de medición neta de EIP-2200, que miran tres valores a la vez: el valor original del slot al empezar la transacción, el valor actual y el valor nuevo que se va a escribir. Las constantes posteriores a Berlin son: un slot frío paga 2100 extra; si el valor original es igual al actual (la transacción aún no ha tocado ese slot), escribir de 0 a un valor distinto de cero cuesta Gsset = 20 000, y escribir de un valor distinto de cero a otro valor, o a 0, cuesta Gsreset = 2900; si la transacción ya ha modificado el slot (el valor original no coincide con el actual), solo se cobra un acceso caliente de 100 gas; una escritura vacía en la que el valor nuevo es igual al actual también cuesta 100.

Las reglas de reembolso se endurecieron con EIP-3529 (actualización London, bloque 12 965 000, 5 de agosto de 2021). El reembolso por reescribir un valor distinto de cero a cero bajó de 15 000 a 4800 (SSTORE_RESET_GAS más ACCESS_LIST_STORAGE_KEY_COST), se eliminó el reembolso de SELFDESTRUCT y el tope de reembolso total por transacción se rebajó a gas_used // 5. El patrón en el que el valor original es 0 y dentro de la transacción primero se escribe un valor distinto de cero y luego se vuelve a 0 sigue generando un reembolso de 19 900 gas (20 000 menos 100), pero también está sujeto al tope de 1/5.

| Operación (criterio Berlin / London) | Gas |
|--------------------------|-----|
| SLOAD, acceso frío / acceso caliente | 2100 / 100 |
| SSTORE, valor original igual al actual, de 0 a distinto de cero | 20 000, más 2100 de acceso frío |
| SSTORE, valor original igual al actual, de distinto de cero a distinto de cero o a cero | 2900, más 2100 de acceso frío; reembolso de 4800 al pasar a cero |
| SSTORE, slot ya modificado por esta transacción | 100 |
| Bloqueo de reentrada 0 → 1 → 0 (misma transacción) | Bruto 22 200, reembolso 19 900 |

Lo que debe persistir entre transacciones va en storage: saldos, propiedad, configuración, contadores acumulativos. El precio tiene dos capas: una es el gas y otra la inflación de estado, porque todos los nodos completos tienen que conservar esos slots a largo plazo. Los reembolsos inducen fácilmente a la ilusión de que «volver a escribir 0 sale gratis»; el criterio real es que el reembolso se liquida solo al terminar la transacción y cubre como máximo el 20 % del consumo total. Una transacción que solo consuma 30 000 gas puede recuperar, en teoría, 6000 gas de reembolso.

## Transient Storage: vale entre llamadas internas y se vacía al terminar la transacción

EIP-1153 introdujo TLOAD (0x5c) y TSTORE (0x5d) en la actualización Cancun (bloque 19 426 587, 13 de marzo de 2024). El direccionamiento es igual al de SLOAD y SSTORE: una dirección de 32 bytes apunta a un valor de 32 bytes. Ambas operaciones cuestan 100 gas fijos; no hay distinción entre frío y caliente, no hay reembolsos y no hace falta reservar costo para una limpieza futura, porque en la especificación nunca se escriben a disco.

El comportamiento se diferencia de storage en tres puntos concretos. En la escala temporal, se pone a cero al terminar la transacción y el valor no se serializa en ninguna estructura persistente. En la semántica de reversión, revertir el marco revierte las escrituras de ese marco, igual que en storage y a diferencia de memory (que se descarta entera cuando el marco retorna o se revierte). En las restricciones de contexto, TSTORE lanza una excepción dentro de STATICCALL, mientras que TLOAD está permitido. Además, EIP-1153 exime explícitamente a TSTORE de la restricción que EIP-2200 impone a SSTORE: no exige que gasleft sea mayor que el subsidio de llamada de 2300.

Este diseño apunta directamente a la «comunicación entre marcos». Antes de EIP-1153, para pasar estado temporal entre contratos había que recurrir a los parámetros y valores de retorno de CALL (que un contrato intermedio no confiable puede manipular) o a escrituras en storage (caras y dependientes del reembolso). Después de que EIP-3529 rebajara el reembolso a 1/5 de gas_used, las transacciones pequeñas prácticamente no recuperan nada. Una escritura de bloqueo 0 → 1 → 0 suma 19 900 gas al contador de reembolsos; despejando del tope gas_used // 5, la transacción entera necesita unos 99 500 gas para recuperar el reembolso completo. Ese 99 500 es el gas_used total deducido de la fórmula del tope de EIP-3529, no un valor que dé el texto del EIP. El cuerpo de EIP-1153 ofrece, desde otro ángulo, la estimación de sus autores: sus propias palabras son que la transacción debe gastar unos 80k gas en «otras operaciones» para obtener el reembolso completo de un bloqueo de reentrada. Las dos cifras usan criterios distintos, pero encajan: 99 500 menos el consumo bruto del propio bloqueo, unos 22 200 gas, da unos 77 300, del mismo orden que la estimación de 80k de los autores para «otras operaciones». El almacenamiento transitorio no participa en el contador de reembolsos, así que esquiva ese umbral.

El bloqueo de reentrada, la autorización de una sola transacción, la comprobación de equilibrio de saldos al cerrar un callback y el paso de metadatos de un contrato proxy hacia aguas abajo encajan bien aquí. Solidity admite variables de estado de tipo valor transient desde la 0.8.28, y la versión de EVM debe fijarse en cancun; los tipos de referencia (arrays, mappings, structs), las variables locales y los parámetros no están soportados y hay que escribir el ensamblador en línea a mano. Esta es la forma mínima del bloqueo de reentrada:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28; // La versión de EVM debe ser cancun

contract TransientLock {
    uint256 transient entered;

    modifier nonReentrant() {
        require(entered == 0, "reentrant");
        entered = 1; // TSTORE, 100 gas
        _;
        entered = 0; // Las llamadas posteriores dentro de la misma transacción leerán 0
    }

    function withdraw() external nonReentrant {}
}
```

Si la versión del compilador o la cadena de destino no cumplen las condiciones, se puede usar directamente ensamblador, con una semántica idéntica:

```solidity
assembly {
    tstore(0, 1)       // Escribe 1 en el slot 0, 100 gas
    let v := tload(0)  // Se lee de vuelta, 100 gas
}
```

## Cuatro consecuencias típicas de un mal uso

Guardar en memory o en almacenamiento transitorio un estado que hay que leer entre transacciones da el síntoma de «el estado se ha perdido». Al terminar la transacción el valor se pone a cero y la siguiente llamada lee el valor por defecto 0; el código no da error, pero la lógica de negocio ya está mal. Estos errores suelen pasar desapercibidos en las pruebas locales, porque a menudo las pruebas se completan dentro de la misma transacción o del mismo entorno simulado.

Guardar en storage un valor intermedio que solo vale dentro de una transacción da el síntoma de un costo descontrolado, y además un costo que depende del tamaño de la transacción. El bloqueo de reentrada y la autorización temporal se pueden implementar con storage desde el punto de vista funcional, pero desde el económico hay que esperar a que la transacción sea lo bastante grande para recuperar el reembolso; cuanto más pequeña es la transacción, más se acerca el costo neto real al costo bruto.

Poner en memory un valor intermedio que debe compartirse entre llamadas da el síntoma de que el marco hijo no lo lee. CALL cambia de marco de ejecución y el hijo recibe una memoria independiente que arranca a cero; lo que escriba el padre no aparecerá en el hijo. Las únicas vías legales de paso son calldata y returndata.

Sustituir con almacenamiento transitorio un mapping que iba en memory da el síntoma de un comportamiento inesperado durante una reentrada. EIP-1153 lo advierte expresamente en sus consideraciones de seguridad: el almacenamiento transitorio no se descarta al retornar una llamada, así que si se usa como si fuera un mapping en memoria, una llamada reentrante dentro de la misma transacción verá los valores que dejó la vuelta anterior. Aparte del problema semántico, un costo de 100 gas por operación también es muy superior al de una escritura en memoria.

Hay una quinta forma de mal uso que se pasa por alto con facilidad: olvidarse de poner a cero. Si el bloqueo de reentrada escribe 1 y en alguna rama retorna antes sin volver a 0, las llamadas posteriores dentro de la misma transacción quedarán bloqueadas por ese cerrojo para siempre. La especificación de EIP-1153 lo dice sin rodeos: solo se deben dejar valores distintos de cero en esos slots si las llamadas posteriores dentro de la transacción realmente los van a usar.

## Límites y puntos inciertos

Todas las cifras de gas del texto llevan su criterio: los precios de acceso frío y caliente posteriores a la actualización Berlin (15 de abril de 2021, bloque 12 244 000), las reglas de reembolso posteriores a London (5 de agosto de 2021, bloque 12 965 000) y el almacenamiento transitorio que introdujo Cancun (13 de marzo de 2024, bloque 19 426 587). La EVM no es una especificación congelada, así que antes de desplegar en varias cadenas hay que confirmar una por una qué EIP implementa la cadena de destino: en una cadena compatible con EVM que se haya quedado en London o antes, TLOAD y TSTORE abortan la ejecución directamente como opcodes ilegales.

El soporte de almacenamiento transitorio de Solidity tiene un defecto de compilador ya corregido: entre la 0.8.28 y la 0.8.33, si se activa la tubería IR (--via-ir) y en la misma unidad de compilación coexisten un delete sobre una variable transitoria y una limpieza de storage persistente del mismo tipo de valor, las funciones auxiliares de limpieza de Yul que se generan se reutilizan por tener el mismo nombre y emiten el opcode equivocado (SSTORE donde debería ir TSTORE, o al revés); quedó corregido en la 0.8.34. El defecto solo afecta a la tubería IR; la tubería legacy no se ve afectada. Los proyectos que usen variables transitorias y compilen con via-ir deben elegir una versión de compilación fuera del intervalo afectado.

Hay dos cosas sin calendario público, que solo se pueden marcar como inciertas. Los 100 gas del almacenamiento transitorio son el valor actual que fijó EIP-1153; no hay por ahora ninguna hoja de ruta pública ni propuesta ya en trámite sobre si alguna futura bifurcación dura los ajustará. La implementación del árbol de estado (por ejemplo, el avance del árbol de Verkle) cambiará la base real de costo de las lecturas de storage; la motivación de EIP-2929 también dice que rediseñar la disposición de la base de datos para que los clientes lean el almacenamiento directamente bajaría aún más el peor tiempo de procesamiento; pero las constantes de tarificación las siguen fijando EIP-2929 y EIP-3529, y puede haber una divergencia prolongada entre ambas cosas. De estos dos puntos no se puede dar una conclusión cuantitativa citable; solo se mantienen como juicio de dirección.

## Fuentes

- EIP-1153, Transient storage opcodes: https://eips.ethereum.org/EIPS/eip-1153
- EIP-2929, Gas cost increases for state access opcodes: https://eips.ethereum.org/EIPS/eip-2929
- EIP-3529, Reduction in refunds: https://eips.ethereum.org/EIPS/eip-3529
- EIP-2200, Structured Definitions for Net Gas Metering: https://eips.ethereum.org/EIPS/eip-2200
- Ethereum Yellow Paper, apéndice G (tabla de tarifas) y fórmula (328) (función de tarificación de memoria): https://ethereum.github.io/yellowpaper/paper.pdf
- ethereum.org, referencia de opcodes (TLOAD y TSTORE, 100 gas cada uno): https://ethereum.org/en/developers/docs/evm/opcodes/
- Notas de la versión 0.8.28 de Solidity (soporte de variables de estado de tipo valor transient): https://www.soliditylang.org/blog/2024/10/09/solidity-0.8.28-release-announcement/
- Documentación de Solidity, Transient Storage (la versión de EVM debe ser cancun; los tipos de referencia y las variables locales aún no están soportados): https://docs.soliditylang.org/en/latest/contracts.html#transient-storage
- Defecto de colisión en la función auxiliar de limpieza del almacenamiento transitorio de Solidity (0.8.28 a 0.8.33; corregido en 0.8.34): https://www.soliditylang.org/blog/2026/02/18/transient-storage-clearing-helper-collision-bug/
- Historial de actualizaciones de red de ethereum.org (alturas de bloque y fechas de cada bifurcación): https://ethereum.org/en/history/

## Lecturas relacionadas

- [Introducción al control de concurrencia optimista (OCC): de las bases de datos a la ejecución en cadena](/es/blog/optimistic-concurrency-control-intro)
- [Glosario de métricas de rendimiento: TPS, BPS, latencia de confirmación, finalidad y tasa de conflicto](/es/blog/performance-metrics-glossary)
- [Por qué una EVM de un solo hilo limita el TPS: historia de la congestión y el modelo de ejecución](/es/blog/evm-single-thread-bottleneck)

Este artículo pertenece a la serie de fundamentos de la EVM; dentro de la misma serie, el 0.15 «ABI en detalle: selector, parámetros estáticos y tipos dinámicos» trata cómo se codifican y decodifican los datos de llamada.
