# Transient Storage (EIP-1153): tstore and tload in Practice
Every Solidity developer knows the pain of SSTORE. Writing a single storage slot from zero can cost 20,000 gas, and even updating a non-zero slot costs 5,000 gas. A lot of that data does not need to survive past the current transaction. Transient storage, introduced by EIP-1153, gives you a second key-value store that lives only for the duration of one transaction and then disappears at no cost. This article explains the two new opcodes, walks through a concrete reentrancy guard, compares the gas, and lists the caveats you must understand before shipping it.
What transient storage actually is
Transient storage is a separate address space inside the EVM, distinct from regular (persistent) storage. It is organized exactly like persistent storage: a mapping from 256-bit keys to 256-bit values, scoped per contract address. The crucial difference is its lifetime.
- Persistent storage is written with
SSTORE, read withSLOAD, and survives across transactions until you overwrite it.
- Transient storage is written with
TSTORE, read withTLOAD, and is automatically wiped to zero at the end of every transaction.
The two opcodes were added in the Cancun upgrade:
TLOAD(opcode0x5c): pops a key from the stack, pushes the stored value.
TSTORE(opcode0x5d): pops a key and a value from the stack, writes the value.
Both are flat-priced. A TLOAD and a TSTORE each cost 100 gas, the same as a warm storage access, with no surprises around zero versus non-zero values or refunds. This predictability is part of what makes transient storage attractive.
Lifetime within a transaction
This is the single most important property, so read it twice. Transient storage is cleared at the end of the transaction, not at the end of a single call frame.
That means a value you TSTORE in one external call is still readable by TLOAD in a later call within the same transaction. If contract A calls contract B which calls back into A, the transient slot written at the start of A's first call is still set when the reentrant call arrives. That is precisely the behavior that makes a reentrancy guard possible.
Keep in mind the boundaries:
- Across two separate transactions: transient storage always starts at zero.
- Within one transaction across nested calls: values persist.
- On revert: changes to transient storage are rolled back, just like persistent storage, so a reverted sub-call leaves the slot at whatever value it held before that sub-call.
Building a reentrancy guard
The classic OpenZeppelin ReentrancyGuard uses a persistent storage slot toggled between two states. It pays SSTORE on entry and SSTORE on exit of every protected function. Transient storage does the same job for far less.
In Solidity 0.8.24 and later you can use inline assembly directly. The opcodes are exposed as tstore and tload:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
abstract contract TransientReentrancyGuard {
// A fixed, arbitrary slot for the lock flag.
// keccak256("transient.reentrancy.guard") - 1 keeps it clear of
// normal storage layout, even though transient space is separate.
bytes32 private constant LOCK_SLOT =
0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00;
error ReentrantCall();
modifier nonReentrant() {
_checkAndLock();
_;
_unlock();
}
function _checkAndLock() private {
assembly {
if tload(LOCK_SLOT) {
// revert with ReentrantCall()
mstore(0x00, 0xab143c06)
revert(0x1c, 0x04)
}
tstore(LOCK_SLOT, 1)
}
}
function _unlock() private {
assembly {
tstore(LOCK_SLOT, 0)
}
}
}
Using it is identical to the persistent version:
contract Vault is TransientReentrancyGuard {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount, "insufficient");
balances[msg.sender] -= amount;
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "transfer failed");
}
}
Because the lock is set with tstore and the transaction-scoped lifetime keeps it set across the reentrant call, a malicious receiver calling back into withdraw hits tload(LOCK_SLOT) != 0 and reverts. When the outer call finishes, _unlock clears the slot, and the automatic end-of-transaction wipe is a safety net even if you forget.
Higher-level syntax
If you are on a recent compiler, you can declare a state variable as transient and skip the assembly entirely:
pragma solidity ^0.8.28;
abstract contract TransientGuard {
bool transient locked;
error ReentrantCall();
modifier nonReentrant() {
if (locked) revert ReentrantCall();
locked = true;
_;
locked = false;
}
}
The transient data location compiles down to the same TSTORE and TLOAD opcodes, but the compiler manages the slot for you. Reading and writing a transient variable is type-safe and far less error prone than hand-written assembly.
Gas savings versus SSTORE
The headline number is simple. A guard backed by persistent storage pays, in the common warm case, around 5,000 gas to set the lock and a smaller amount to reset it (a storage refund applies on the reset, but refunds are capped per transaction and never fully recover the cost). A transient guard pays a flat 100 gas for tstore and 100 gas for tload.
| Operation | Persistent storage | Transient storage |
| --- | --- | --- |
| Write a fresh slot | 20,000 gas | 100 gas |
| Update a warm slot | 5,000 gas | 100 gas |
| Read a warm slot | 100 gas | 100 gas |
| End-of-tx cleanup | manual reset, partial refund | automatic, free |
For a reentrancy guard the practical saving is on the order of a few thousand gas per protected call. The win is even larger for patterns that previously abused persistent storage as a scratchpad inside a single transaction, such as flash-accounting in an AMM, callback context for flash loans, or passing data between calls without touching calldata.
Real use cases beyond reentrancy
- Flash accounting: a DEX can record token deltas in transient storage during a multi-step swap and require them to net to zero before the transaction ends, avoiding repeated
SSTOREwrites.
- Reusable callback context: a flash loan provider can stash the borrower and expected repayment in transient slots, then read them back in the callback without encoding everything into
calldata.
- Per-transaction caches: any value that is expensive to recompute and only needed within one transaction can live in transient storage instead of memory that does not cross call frames.
Caveats you must respect
TSTORE surviving a reverted sub-call.SSTORE, it is a different tool.cancun or later) before deploying.Practice it hands-on
The fastest way to internalize transient storage is to write both versions of the guard, deploy them, and watch the gas reports diverge. You can do exactly that, with an interactive editor and gas comparisons, in the smart contract exercises on app.solingo-blockchain.xyz. Try replacing a persistent guard with a transient one, then add a malicious receiver and confirm the reentrant call still reverts. Seeing the lock hold across the reentrant frame is the moment the lifetime rule clicks.