Skip to main content

How to Build and Deploy Reth Execution Extensions (ExExs)

Updated on
Sep 19, 2025

14 min read

Overview

Reth Execution Extensions (ExExs) are plugins that run inside the Reth execution client, allowing you to process blockchain data in real time as blocks are committed. Unlike external applications that poll RPC endpoints, ExExs receive direct notifications from the Ethereum execution engine, making them very efficient for indexing, and custom processing logic.

This guide will walk you through creating, building, and deploying a transaction counter ExEx that demonstrates the core concepts and patterns you'll need to build your own custom extensions.

What You Will Learn


  • Understand the architecture and lifecycle of Reth ExExs
  • Create a custom transaction counter ExEx from scratch
  • Deploy ExExs as systemd services in production
  • Test and monitor ExEx performance

What You Will Need


  • A running Reth node - Follow our Reth node setup guide first
  • Rust development environment (rustc 1.86+)
  • Linux, macOS, or Windows with WSL
  • Git installed
  • Basic familiarity with Rust programming
  • Understanding of Ethereum block structure and transactions

Important: ExExs are not separate processes. They compile into and run inside the same Reth binary, sharing memory directly with the execution engine. This means you cannot run an ExEx against an existing Reth node, you must rebuild Reth with your ExEx included.

What are Reth Execution Extensions (ExExs)?

Reth Execution Extensions are Rust modules that hook directly into the Reth execution client's block processing pipeline. When new blocks are committed, reverted, or reorganized, ExExs receive immediate notifications with full access to block data, transactions, receipts, and state changes.

ExEx vs External Applications


AspectExExExternal RPC App
Latency~1ms (direct memory)50-500ms (network + processing)
Data AccessFull block data, receipts, stateLimited by RPC methods exposed
Resource UsageShared process memorySeparate process + network
DeploymentRebuild Reth binaryIndependent deployment
ScalabilitySingle process scalingHorizontal scaling

ExEx Architecture and Lifecycle

ExExs are not separate programs that connect to Reth over the network. Instead, ExExs compile directly into the same Reth binary and runs alongside the node, sharing memory and processing power. This integration is why ExExs can process blockchain data with such low latency.

When you run your ExEx, you're actually running a modified version of Reth that includes your custom logic. The node processes blocks normally, but now it also sends notifications to your ExEx code running in the same process. The following are the notification types:

Notification Types

ExExs receive three types of notifications:


  1. ChainCommitted: New blocks added to the canonical chain
  2. ChainReverted: Blocks removed due to chain reverts
  3. ChainReorged: Chain reorganization (old chain out, new chain in)

We'll now cover the lifecyle and how we'll implement the notification types (specifically ChainCommited) to count transactions in a block.

ExEx Lifecycle

// 1. Initialization
async fn exex_init<Node: FullNodeComponents>(
ctx: ExExContext<Node>,
) -> eyre::Result<impl Future<Output = eyre::Result<()>>> {
// Setup databases, connections, etc.
Ok(exex(ctx))
}

// 2. Main processing loop
async fn exex<Node: FullNodeComponents>(mut ctx: ExExContext<Node>) -> eyre::Result<()> {
while let Some(notification) = ctx.notifications.try_next().await? {
// Process notification
handle_notification(&notification).await?;

// Signal completion for pruning
if let Some(committed_chain) = notification.committed_chain() {
ctx.events.send(ExExEvent::FinishedHeight(
committed_chain.tip().num_hash()
))?;
}
}
Ok(())
}

You'll see later that the transaction counter example we build will only use the exex function as we don't need to keep state as the chain progresses. However, for future ExExs, you can use the exex_init function when you need to:


  • Load previous state from a database
  • Connect to external services
  • Set up storage
  • Additional initialization logic

Now that we have a better understanding of Reth's ExExs, let's start the technical coding portion of the guide.

Project Setup

For this guide, we'll demonstrate building a transaction counter ExEx that tracks and counts transactions for each block. This will be a simple example, but it will be foundational for building future ExExs.

Important: Before proceeding, please ensure you have a Reth full node running and synced. For this guide, we'll be using a synced Hoodi node.

Prerequisites Check

First, verify your environment:

# Check Rust version (need 1.86+)
rustc --version

# Check if you have a running Reth node
systemctl status reth.service

# Verify Git installation
git --version

You'd also want to ensure your Reth node is synced and at the tip of the chain (e.g., check eth_syncing, or get the latest block from your local node and compare with block explorer)

Create ExEx Project


  1. Create a new Rust project:
mkdir transaction-counter-exex
cd transaction-counter-exex

  1. After, initialize the Cargo project by creating a Cargo.toml file:
touch Cargo.toml

Then include the following code in the file:

[package]
name = "transaction-counter-exex"
version = "0.1.0"
edition = "2021"
rust-version = "1.86"
license = "MIT OR Apache-2.0"
publish = false

[dependencies]
# Core Reth dependencies
reth = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.6.0" }
reth-exex = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.6.0" }
reth-node-ethereum = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.6.0" }
reth-node-api = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.6.0" }
reth-tracing = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.6.0" }
reth-primitives = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.6.0" }

# Ethereum primitives
alloy-primitives = { version = "1.0", default-features = false }
alloy-consensus = { version = "1.0", default-features = false }

# Async and utilities
futures = "0.3"
eyre = "0.6"

[dev-dependencies]
tokio = { version = "1.0", features = ["full"] }

The core dependencies include reth-exex for the ExEx framework, reth-node-ethereum for Ethereum-specific functionality, and reth-tracing for logging. The Alloy libraries provide standard Ethereum types like addresses and block structures, while futures handles the async stream processing needed to receive blockchain notifications, and eyre provides better error messages when something goes wrong. The tokio dependency in the dev-dependencies section is only used for testing and provides the async runtime.


  1. Then, create the source (src) directory and main.rs file:
mkdir src
touch src/main.rs

Implementing the Transaction Counter ExEx

Now let's implement a transaction counter that categorizes transactions by type.

In the src/main.rs, add the following code:

use futures::{Future, TryStreamExt};
use reth_exex::{ExExContext, ExExEvent, ExExNotification};
use reth_node_api::{BlockBody, FullNodeComponents};
use reth_node_ethereum::EthereumNode;
use alloy_consensus::BlockHeader;
use reth_tracing::tracing::info;

#[derive(Debug, Default, Clone)]
struct TransactionCounter {
/// Total number of transactions processed
total_transactions: u64,
/// Number of blocks processed
total_blocks: u64,

/// FUTURE USAGE EXAMPLES BELOW
/// Number of ETH transfers (transactions with no input data)
/// eth_transfers: u64,
/// Number of contract calls (transactions with input data to existing contracts)
/// contract_calls: u64,
}

/// The initialization logic of the ExEx is just an async function.
async fn exex_init<Node: FullNodeComponents>(
ctx: ExExContext<Node>,
) -> eyre::Result<impl Future<Output = eyre::Result<()>>> {
Ok(exex(ctx))
}

/// An ExEx that counts transactions and blocks.
async fn exex<Node: FullNodeComponents>(mut ctx: ExExContext<Node>) -> eyre::Result<()> {
let mut counter = TransactionCounter::default();

while let Some(notification) = ctx.notifications.try_next().await? {
match &notification {
ExExNotification::ChainCommitted { new } => {
// Count blocks and transactions in the committed chain
for block in new.blocks_iter() {
counter.total_blocks += 1;

// Count transactions in this block
let tx_count = block.body().transactions().len() as u64;
counter.total_transactions += tx_count;

info!(
block_number = block.number(),
block_hash = %block.hash(),
tx_count,
total_transactions = counter.total_transactions,
total_blocks = counter.total_blocks,
"Processed block"
);
}

info!(
committed_chain = ?new.range(),
total_transactions = counter.total_transactions,
total_blocks = counter.total_blocks,
"Processed committed chain"
);
}
ExExNotification::ChainReorged { old, new } => {
info!(
from_chain = ?old.range(),
to_chain = ?new.range(),
"Received reorg"
);
// For simplicity, we just log reorgs
// In production, you might want to adjust counters
}
ExExNotification::ChainReverted { old } => {
info!(reverted_chain = ?old.range(), "Received revert");
// For simplicity, we just log reverts
}
}

if let Some(committed_chain) = notification.committed_chain() {
ctx.events
.send(ExExEvent::FinishedHeight(committed_chain.tip().num_hash()))?;
}
}

Ok(())
}

fn main() -> eyre::Result<()> {
reth::cli::Cli::parse_args().run(|builder, _| async move {
let handle = builder
.node(EthereumNode::default())
.install_exex("TransactionCounter", exex_init)
.launch()
.await?;

handle.wait_for_node_exit().await
})
}

Save the file. Let's recap the code above.

This code creates a simple Reth ExEx that counts transactions and blocks in real-time. The TransactionCounter struct holds two counters: total_transactions and total_blocks.

The main exex function runs an infinite loop listening for blockchain notifications. When it receives a ChainCommitted event, it iterates through each new block, counts the transactions in that block using block.body().transactions().len(), adds to the running totals, and logs the results. It also handles reorg and revert events by logging it.

The code follows Reth's standard ExEx pattern with exex_init returning the main processing, and main using the node builder to install the ExEx as TransactionCounter and launch the Reth node. After processing each chain, it sends a FinishedHeight event back to Reth to signal completion.

Building the ExEx


  1. Compile your ExEx:
cargo check

If you encounter compilation errors, ensure all imports are correct and your Rust version is 1.86+.

  1. Build in release mode for production:
cargo build --release

Note: The initial build may take 10-15 minutes as it compiles the entire Reth codebase along with your ExEx.

Production Deployment

Systemd Service Setup

For production deployments, you'll want to run your ExEx as a systemd service. This ensures automatic startup, restart on failure, and proper logging.


Creating a New Service

If you want to run the ExEx as a separate service (recommended for testing):

# Create the service file
sudo nano /etc/systemd/system/reth-exex.service

Add the following configuration:

> Note: Replace /path/to/transaction-counter-exex with your actual project directory path, YOUR_USERNAME with your system username, and /path/to/jwtsecret with your JWT secret file location.

[Unit]
Description=Reth Ethereum Client with Transaction Counter ExEx
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=YOUR_USERNAME
Group=YOUR_USERNAME
ExecStart=/path/to/transaction-counter-exex/target/release/transaction-counter-exex node --chain hoodi --http --http.addr 0.0.0.0 --http.port 8545 --authrpc.addr 127.0.0.1 --authrpc.port 8551 --authrpc.jwtsecret /path/to/jwtsecret --full
WorkingDirectory=/path/to/transaction-counter-exex
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=reth-exex
KillMode=mixed
KillSignal=SIGINT
TimeoutStopSec=30

# Security settings
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=false
ReadWritePaths=/path/to/transaction-counter-exex
ReadWritePaths=/path/to/.local/share/reth
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true

[Install]
WantedBy=multi-user.target

Deploy the service:

# Reload systemd
sudo systemctl daemon-reload

# Stop existing Reth service if running
sudo systemctl stop reth.service

# Start ExEx service
sudo systemctl start reth-exex.service
sudo systemctl enable reth-exex.service

# Monitor logs
sudo journalctl -u reth-exex.service -f

Testing the Transaction Counter ExEx

Let's verify our transaction counter is relaying back accurately. To do so, open a new terminal window and run the following commands:

journalctl -u reth-exex.service --since="1 minutes ago" | grep -E "(TransactionCounter|Processed block)"

This command will return the blocks processed in the last minute. Note that you may need to update this if you're using a network other than Hoodi.

You should see a response like the below, indicating our TransactionCounter is running.

2025-09-18T15:20:01.947825Z  INFO exex{id="TransactionCounter"}: Processed block block_number=1238975 block_hash=0x99acbc7b5892f32afbdce65871a7ee8691789179eaf20eed8643776b91b40eb7 tx_count=39 total_transactions=10135 total_blocks=259
Sep 18 15:20:01 ip-172-31-6-79.us-east-2.compute.internal reth-exex[2796328]: 2025-09-18T15:20:01.947862Z INFO exex{id="TransactionCounter"}: Processed committed chain committed_chain=1238975..=1238975 total_transactions=10135 total_blocks=259

Explorer link

Next, run the cURL command below to get the latest block from your Reth node, which we'll use for testing.

curl -s -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
http://localhost:8545

If you're running on a different port than 8545, update as needed.

You'll get a response like {"jsonrpc":"2.0","id":1,"result":"0x12e7bf"}. We need to convert the hexadecimal output into decimal. We can do that with the following command:

echo $((0xYourHex))

With that response, let's make another cURL call to parse out the number of transactions in our test block (e.g., )

curl -s -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0xYourHex",false],"id":1}' \
http://localhost:8545 | jq '.result.transactions | length'

The response shows as 39, we can confirm that our ExEx is accurately logging the information by viewing the explorer link of that specific block number.

Alternatives Solutions via QuickNode

If you don't want to deal with the hassle of managing your own node or building end-to-end pipelines. Check out some of the powerful solutions and APIs below:

Streams

Streams pushes blockchain data directly to your destination without complex ETL pipelines. It delivers both real-time and historical data with built-in reorg handling.

Key Features:


  • Real-time and historical data delivery
  • JavaScript filters for custom logic
  • Multiple destinations (Webhooks, S3, PostgreSQL)
  • Automatic reorg handling
  • Troubleshooting features that allow you to configure retry, wait period, and pause mechanisms when there's an error
  • Build a complete indexer in under 250 lines of code

Webhooks

Set up one-click alerts for on-chain events, delivered directly to your server with QuickNode Webhooks.

Key Features:


  • Real‑time delivery: Receive events that are important to you in real time
  • Guided wizard: Pick a chain, select a no-code template or write your own filter, set your URL, and that's it
  • Compression: Compress payloads with gzip to reduce payload size and improve latency
  • Learn how to get started in this technical guide

Marketplace

Instead of needing to build and manage custom infrastructure, explore the QuickNode Marketplace to take advantage of custom APIs already built and ready to use, such as:


Final Thoughts

Congratulations! You've successfully learned how to build, deploy, and manage Reth Execution Extensions. ExExs provide a powerful way to extend Reth's functionality while maintaining high performance and reliability.

Whether you're building analytics platforms, MEV strategies, indexing services, or custom business logic, ExExs provide the foundation for performant blockchain applications.

Next Steps


  • Explore the official Reth ExEx examples for more patterns
  • Join the Reth Discord community for support and discussions
  • Consider contributing your ExEx patterns back to the ecosystem
  • Experiment with advanced features like state access and custom RPC methods

Subscribe to our newsletter for more articles and guides on Ethereum infrastructure and development. If you have any feedback, feel free to reach out to us via X. You can always chat with us on our Discord community server, featuring some of the coolest developers you'll ever meet!

We ❤️ Feedback!

Let us know if you have any feedback or requests for new topics. We'd love to hear from you.

Share this guide