What is sdk.Context
Every message handler, keeper method, and block hook in the Cosmos SDK receives an sdk.Context. It is the execution environment for a single unit of work (a transaction, a query, or a block hook) and carries everything that code needs to read state, emit events, and consume gas. Rather than passing the store, gas meter, and block header as separate arguments to every function, Context bundles them into a single value.
The Context struct is defined in types/context.go:
With* methods that return a new copy. This means a module can safely derive a sub-context (for example, with a different gas meter) without affecting the caller’s context.
Block metadata
Context exposes read-only access to the current block’s metadata (seetypes/context.go):
ctx.BlockHeight()returns the current block number.ctx.BlockTime()returns the block’s timestamp.ctx.ChainID()returns the chain identifier string.ctx.Logger()returns a structured logger scoped to the current execution context. Modules use this for operational logging (e.g., logging an upgrade activation or an unexpected state) without affecting consensus.
BaseApp from the block header provided by CometBFT before any block logic runs. Modules read them to implement time-dependent logic (for example, checking whether a vesting period has elapsed) or to tag events with the block height.
ctx.IsCheckTx() returns true when the context is being used for mempool validation rather than block execution. For finer-grained branching, ctx.ExecMode() returns the precise execution mode: ExecModeCheck, ExecModeReCheck, ExecModeSimulate, ExecModePrepareProposal, ExecModeProcessProposal, ExecModeFinalize, and others (see types/context.go for more details). Modules that need to behave differently during simulation or proposal handling use ExecMode() instead of IsCheckTx().
Context and state access
State is accessed through context. The context holds a reference to the multistore, and each keeper opens its own store through the context:Atomic sub-execution with CacheContext
Modules that need to attempt a sub-operation and revert it on failure can call ctx.CacheContext(), which returns a branched copy of the context and a writeCache function. All state changes in the sub-operation go into the branch. Calling writeCache() flushes them to the parent context; not calling it discards them atomically.
Gas metering
What gas measures
Gas is a unit of computation. In the Cosmos SDK, gas accounts for both computation and state access. Every store read, store write, and iterator step costs gas. Complex computations such as signature verification in theAnteHandler also cost gas.
The gas system exists to prevent abuse. Without a gas limit, a single transaction could exhaust a node’s resources with an unbounded computation or an unindexed state scan.
Gas limit and the transaction gas meter
Every transaction specifies a gas limit in itsauth_info.fee.gas_limit field. When BaseApp begins executing a transaction, it creates a GasMeter initialized with that limit and attaches it to the context.
The GasMeter interface provides two key methods:
GasConsumed returns the total gas used so far in the current execution unit. ConsumeGas adds to the running total and panics with ErrorOutOfGas if consumption exceeds the limit.
How gas is consumed
Gas is consumed automatically at the store layer. Every read and write through theGasKVStore wrapper charges gas before delegating to the underlying store:
- A
Get(store read) charges a flat read cost plus a per-byte cost for the key and value. - A
Set(store write) charges a flat write cost plus a per-byte cost for the key and value.
ctx.GasMeter().ConsumeGas(...) directly only for computation costs that are not captured by store operations (for example, a module that performs a cryptographic operation outside the store).
When gas runs out
If gas is exhausted during execution,ConsumeGas panics with ErrorOutOfGas. BaseApp recovers from this panic, discards the current message execution branch, and returns an error to the user. Fees may still be charged for the gas consumed up to the point of failure, and AnteHandler side effects may already have been applied before message execution started.
Block gas limit
In addition to the per-transaction gas meter, there is a block-level gas meter that tracks total gas consumed by all transactions in a block. The block gas limit prevents a single block from consuming unbounded computation. If a transaction would cause the block’s gas total to exceed the limit, it is excluded from the block.Events
What events are
Events are observable signals emitted during transaction and block execution. A module emits events to describe what happened: tokens were transferred, a validator was slashed, a governance proposal passed. Events carry structured key-value data alongside a type string. Events are not part of consensus state. They are not stored in the KVStore, do not affect the app hash, and are not required for deterministic execution. Instead, they are collected byBaseApp and included in the block result, where indexers, explorers, and relayers consume them.
EventManager
Modules emit events through theEventManager, which is attached to the context.
The EventManager is created fresh for each transaction and collects all events emitted during that execution.
Emitting events
Modules emit events usingEmitEvent or EmitTypedEvent:
EmitEvent appends a raw key-value event to the manager’s accumulated list.
For events backed by protobuf message types, EmitTypedEvent serializes the message’s fields into event attributes automatically:
EmitTypedEvent is the modern approach. It provides type safety and makes the event schema explicit through proto definitions, allowing clients to deserialize events back into typed structs.
Block events and transaction events
Events emitted duringBeginBlock or EndBlock hooks are block events: they describe things that happened at the block level (inflation minted, validator updates applied). Events emitted inside a message handler are transaction events: they describe what a specific transaction did.
Both types are included in the FinalizeBlock response that CometBFT returns to the network, but they are reported separately so clients can distinguish block-level activity from per-transaction activity.
Who consumes events
Events are consumed outside the node:- Block explorers index events to show users what happened in a transaction (which tokens moved, which validator was slashed, which proposal passed).
- Relayers (IBC) subscribe to specific event types to detect packet sends and acknowledgments.
- Indexers and off-chain services build queryable databases of chain activity from event streams. Events can also be queried via the node’s REST API and WebSocket endpoint.
- Wallets and UIs display event data to users as transaction receipts.
Putting it together
During transaction execution, context, gas, and events work together as the runtime layer:app/, and how all the pieces are assembled.