Overview
Backfilling is the process of retrieving historical blockchain data to populate databases, analyze past trends, or audit transactions. Whether you need to index an entire chain from the Genesis block or just catch up on the last 24 hours of activity, retrieving historical data efficiently is a critical infrastructure challenge.
Streams makes backfilling simple with one-click templates, server-side filtering, batching and compression options, and guaranteed delivery to your preferred destination. Instead of writing complex scripts to poll RPC endpoints, you configure a Stream, set your block range, optionally apply filters to extract only the data you need, and Streams pushes the data to your destination (e.g., Webhook, S3, PostgreSQL, Azure Storage, etc.) reliably and at scale.
Why Use Streams for Backfilling?
Streams handles the infrastructure side such as retries, block ordering, and error handling, letting you focus exclusively on your application’s data logic.
Server-Side Filtering and Transformation
Filtering allows you to process data before it leaves Quicknode, ensuring you only receive the data you need.
-
Custom Payloads: Beyond simple filtering, you can modify the data structure to match your schema. For example, you can return only transaction hashes instead of full transaction objects, or use built-in helper functions like
decodeEVMReceiptsto transform raw hex data into human-readable formats. -
Templates: Jumpstart your logic with pre-built templates for common use cases available directly in the Stream filter editor. You can also use one-click templates to quickly create a filter for a specific use case.
-
Rapid Development: Use
console.log()for debugging, and download raw data for specific blocks to understand payload structures while building your filter logic. -
Key-Value Store Integration: Use the Key-Value Store to store and manage extensive lists or key-value sets, then reference them within Streams filters to efficiently evaluate, update, or match against blockchain data. You can also retrieve stored values via REST for use outside of Streams.
Optimized Data Delivery (Batching & Compression)
While optional, enabling batching and compression is highly recommended for backfills to improve speed and reduce costs.
-
Batching: Instead of one HTTP request per block, Streams can send multiple blocks in a single payload. This reduces HTTP overhead and significantly increases throughput.
-
Compression: You can enable Gzip compression for your payloads. This lowers bandwidth usage, which directly reduces costs, and speeds up data transfer for data-heavy blocks.
Learn how to reduce costs with Streams compression: Reduce Streams Costs with Data Compression
Scalability & Automation
Backfilling often requires indexing multiple chains simultaneously. While a single Stream targets one chain, the architecture is designed to scale horizontally.
-
Multichain Support: You can run multiple Streams to backfill ranges of historical blocks on Ethereum, Solana, Bitcoin, or any other supported chains.
-
Automation: You can use the Quicknode Streams REST API to create and manage Streams programmatically or duplicate an existing Stream in the UI.
Transition to Real-Time
Streams can also be used to retrieve real-time data after the backfill is complete. You can configure your Stream to start from a specific block range and then continue streaming new blocks as they are produced, ensuring you always have up-to-date data without needing to switch tools or create a new Stream.
To ensure a smooth transition from historical indexing to live monitoring, Streams offers several advanced features:
-
Elastic Batch: When enabled along with batching, it automatically reduces the batch size to 1 to prioritize low latency as you approach the latest block.
-
Latest Block (Slot) Delay: Once synced with the tip, you can set a delay (e.g., 2 blocks/slots) to reduce reorg risks for real-time data.
-
Reorg Handling: In addition to the above, Streams can automatically detects chain reorganizations and sends correction payloads, ensuring your database remains consistent with the canonical chain.
How to Backfill Data with Streams
-
Select your chain and network. See the supported chains for Streams.
-
Define your block range. You can choose to start from the Genesis block or a specific block height. For the end block, you can set it to a specific height or choose continuous streaming to keep receiving new blocks.
-
Select your dataset. Streams provide different datasets such as Block, Block with Receipts, Transactions, Logs, etc. Choose the one that fits your use case. See Backfill Data by Ecosystem below for chain-specific datasets.
-
Apply filters (optional). Use server-side filtering to narrow down the data you want to receive.
-
Choose your destination. Configure where you want the data to be sent, such as Webhooks, S3, PostgreSQL, etc. Check out the Streams Destinations documentation for more details.
-
Check the connection and send a test payload in the Stream configuration page to ensure everything is set up correctly.
-
Start the Stream.
Note: Not sure which settings to choose? See our Tips for Backfilling with Streams below for our tips and recommendations.
Backfill Data by Ecosystem
Streams can be used to backfill data for any ecosystem, including Ethereum, Bitcoin, Solana, and more. Since they might have different data structures and formats, here are some examples of how you can use Streams to backfill data for different ecosystems.
- Ethereum and EVM Chains
- Solana
- Bitcoin
- XRP Ledger
Backfill Data for Ethereum and EVM Chains
Streams support many EVM chains, including Ethereum, Base, Arbitrum, and BNB Smart Chain. All EVM chains share a similar architecture and data structure, so filters and payload formats are generally the same across all chains, while some chain-specific differences may exist.
Decoding EVM Data
When working with EVM-compatible chains, you can verify and parse data more easily using the decodeEVMReceipts function. This utility transforms raw hex data into human-readable formats by taking raw transaction receipts and your contract ABIs as inputs.
The decoding process automatically:
- Matches event signatures in transaction logs with the provided ABIs.
- Decodes parameters according to their types (addresses, integers, strings, etc.).
- Returns structured data with named parameters in a
decodedLogsobject.
Learn more about this function: Decoding EVM Data Filters
Available Data Sources
For EVM chains such as Ethereum, Base, Arbitrum, and BNB Smart Chain, you can use the following datasets to backfill data.
For the full JSON specification and details of each dataset, please refer to the Streams Data Sources documentation.
| Data Source | Description |
|---|---|
| Block | An array of block objects as returned by eth_getBlockByNumber. |
| Block with Receipts | An array of objects containing a composite dataset with block and receipts as returned by eth_getBlockByNumber and eth_getBlockReceipts. |
| Transactions | An array of arrays of transaction objects, as they appear in the transactions array of block data. |
| Logs | An array of arrays of log objects, as they appear within the logs array in transaction receipts. |
| Receipts | An array of arrays, each containing receipt objects as returned by eth_getBlockReceipts. |
| Traces (debug_trace) | An array of arrays of trace data as returned by debug_traceBlock. |
| Traces (trace_block) | An array of arrays of trace data as returned by trace_block. |
| Block with Receipts + debug_trace | An array of objects containing a composite dataset with block, receipts, and traces from debug_traceBlock. |
| Block with Receipts + trace_block | An array of objects containing a composite dataset with block, receipts, and traces from trace_block. |
Note: Data sources availability varies by chain. Some datasets (specifically Traces) may not be supported on all EVM networks. Check the Streams Data Sources page for the current support matrix.
Example Filter and Response
The sample function below filters a block of transactions to identify and decode standard ERC-20 token transfers by checking the input data for the transfer method signature.
Example filter function:
// Chain: Ethereum
// Dataset: Transactions
// Test with block: 23977403
function main(payload) {
const filteredTransactions = [];
// The standard ERC-20 transfer(address,uint256) method signature
const transferMethodId = "0xa9059cbb";
// Loop through all blocks in the batch
for (const transactions of payload.data) {
for (const transaction of transactions) {
// Ensure the transaction object and input data are valid
if (typeof transaction === 'object' && transaction !== null && typeof transaction.input === 'string') {
// Check if the transaction input starts with the transfer method ID
if (transaction.input.startsWith(transferMethodId)) {
// Decode the 'to' address (skipping method ID and padding)
const toAddress = "0x" + transaction.input.substr(34, 40);
// Decode the 'value' (amount) from the subsequent hex data
const value = BigInt("0x" + transaction.input.substr(74));
filteredTransactions.push({
txHash: transaction.hash,
fromAddress: transaction.from,
toAddress: toAddress,
amount: value.toString(),
tokenContract: transaction.to,
blockNumber: transaction.blockNumber,
});
}
}
}
}
// Return the filtered list, or null if no transfers were found (to save costs)
return filteredTransactions.length > 0 ? { transactions: filteredTransactions } : null;
}
Example response:
{
"transactions": [
{
"amount": "80007920",
"blockNumber": "0x16dddbb",
"fromAddress": "0xa80f9793051cd1f428ad61b276d431f30dd59b6a",
"toAddress": "0xabd22d07c199a56bafa5e2add3cde1127bc98292",
"tokenContract": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"txHash": "0x013a9cdd1de2ade97a1d79178fe59f2025e41495db050aab0cc706cd2be3b2fe"
},
{
"amount": "71839000",
"blockNumber": "0x16dddbb",
"fromAddress": "0x828ee64b59f33e6c3a6b8d4ad8298aeb65421445",
"toAddress": "0xbb03a0b5159d985c304ab183b80e884ae38c9c89",
"tokenContract": "0xdac17f958d2ee523a2206206994597c13d831ec7",
"txHash": "0x2ae18c8918de4744df1d93729e75f89f402723630982224654d18cae047ec74b"
},
// ...
]
}
Additional Resources
- Technical guide: How to Backfill Ethereum ERC-20 Token Transfer Data
Backfill Data for Solana
Streams supports backfilling historical slot ranges on Solana. Due to the network's high throughput and massive data volume, backfills are typically used to index specific windows of time (e.g., retrieving the last 1-2 weeks of data) rather than the entire ledger history from Genesis.
For the full JSON specification and details of each dataset, please refer to the Streams Data Sources documentation.
Free Trial accounts can only create Solana Streams that follow the tip of the blockchain in real time. Historical backfills on Solana Streams are only available on paid plans.
Available Data Sources
| Data Source | Description |
|---|---|
| Block | An array of block objects as returned by getBlock. |
| Programs + Logs | An array of objects containing log messages and transaction metadata relating to program invocations. |
Example Filter and Response
The sample function below filters a block for successful transactions involving a specific Solana account and calculates the SOL balance change (delta) for that account in each transaction.
Example filter function:
// Chain: Solana
// Dataset: Block
// Test with slot: 282164688
// Configuration: Define the target account and filtering preferences
const FILTER_CONFIG = {
// The Public Key of the account to track (e.g., a specific wallet or program)
accountId: '9kwU8PYhsmRfgS3nwnzT3TvnDeuvdbMAXqWsri2X8rAU',
// Set to true to ignore failed transactions (recommended to reduce noise)
skipFailed: true,
};
function main(payload) {
const matchedTransactions = [];
// Loop through all blocks in the batch
for (const block of payload.data) {
for (const tx of block.transactions) {
const result = processTransaction(tx, block);
if (result) {
matchedTransactions.push(result);
}
}
}
return matchedTransactions.length > 0 ? { matchedTransactions } : null;
}
// Helper function to process individual transactions
function processTransaction(transactionWithMeta, block) {
const { meta, transaction } = transactionWithMeta;
// 1. Skip failed transactions if configured to do so
if (FILTER_CONFIG.skipFailed && meta.err !== null) {
return null;
}
// 2. Find the index of our target account within the transaction's account keys
const accountIndex = transaction.message.accountKeys.findIndex(
account => account.pubkey === FILTER_CONFIG.accountId
);
// If the target account is not involved in this transaction, skip it
if (accountIndex === -1) {
return null;
}
// 3. Retrieve balance information using the account index
const preBalance = meta.preBalances[accountIndex];
const postBalance = meta.postBalances[accountIndex];
const delta = postBalance - preBalance;
// 4. Skip transactions where the account's balance did not change
if (delta === 0) {
return null;
}
// 5. Construct and return the custom payload
return {
signature: transaction.signatures[0],
slot: block.parentSlot + 1,
blockTime: block.blockTime,
accountKey: FILTER_CONFIG.accountId,
preBalance,
postBalance,
delta,
};
}
Example response:
{
"matchedTransactions": [
{
"accountKey": "9kwU8PYhsmRfgS3nwnzT3TvnDeuvdbMAXqWsri2X8rAU",
"blockTime": 1723055487,
"delta": -25000000000000,
"postBalance": 485830395323,
"preBalance": 25485830395323,
"signature": "2nWu9XYxKHWNiwGDHLnHYqrF3uGZCN5subE3rbuFqnxwoGQ11FxSEoz6CffmssYhqC43ewDyhiAhvPZNMSzbqVMC",
"slot": 282164688
}
]
}
Additional Resources
- Technical guide: How to Backfill Solana Transaction Data
Backfill Data for Bitcoin
Streams supports backfilling for Bitcoin and Bitcoin Cash. These chains use the UTXO (Unspent Transaction Output) model, which differs from account-based blockchains.
For the full JSON specification and details of each dataset, please refer to the Streams Data Sources documentation.
| Data Source | Description |
|---|---|
| Block | An array of objects as returned by Blockbook's bb_getBlock. |
Example Filter and Response
The sample function below filters the stream to track "whale" activity by identifying and returning only transactions with a value of 1 BTC or greater.
Example filter function:
// Chain: Bitcoin
// Dataset: Block
// Test with block: 927171
function main(payload) {
const {
data,
metadata,
} = payload;
// Define the threshold: 1 BTC in Satoshis (100,000,000 sats = 1 BTC)
const MIN_VALUE = 100000000;
const results = [];
// Iterate through each block in the batch (Streams may deliver multiple blocks at once)
for (const block of data) {
// Iterate through all transactions in the current block
for (const tx of block.txs) {
// Filter logic: Check if the transaction value meets our threshold
if (parseInt(tx.value) >= MIN_VALUE) {
// Construct a simplified custom payload with only relevant details
results.push({
txid: tx.txid,
blockHeight: tx.blockHeight,
blockTime: tx.blockTime,
// Convert Satoshis to BTC for human readability
valueBTC: parseInt(tx.value) / 100000000,
fees: tx.fees
});
}
}
}
// Return the array of high-value transactions, or null to skip the block if none found
return results.length ? results : null;
}
Example response:
[
{
"blockHeight": 927171,
"blockTime": 1765316511,
"fees": "0",
"txid": "fbc62d0e65f2bed9e193480b33adc6f117ba44c839928becadfb61f817f6e7fb",
"valueBTC": 3.14554746
},
{
"blockHeight": 927171,
"blockTime": 1765316511,
"fees": "5640",
"txid": "494ff35569b01a799db0ef0975923844faa46bd1a57a8625cd44b152ded3b661",
"valueBTC": 6.92443529
},
// ...
]
Backfill Data for XRP Ledger
Streams allows you to retrieve historical data from the XRP Ledger (XRPL), delivering complete ledger objects to your destination.
For the full JSON specification and details of each dataset, please refer to the Streams Data Sources documentation.
| Data Source | Description |
|---|---|
| Ledger | An array of ledger objects as returned by ledger. |
Example Filter and Response
The sample function below filters the stream to retain only successful Payment transactions, discarding other transaction types and failed attempts to ensure a clean dataset of value transfers.
Example filter function:
// Chain: XRP Ledger
// Dataset: Ledger
// Test with ledger: 100768299
function main(payload) {
const results = [];
const {
data,
metadata,
} = payload;
// Iterate through each ledger in the batch (Streams may deliver multiple ledgers at once)
for (const item of data) {
const ledger = item.ledger;
// Loop through all transactions in the current ledger
for (const tx of ledger.transactions) {
// Filter Logic:
// 1. Check if it is a 'Payment' type (standard value transfer)
// 2. Check if the transaction succeeded ('tesSUCCESS')
if (
tx.TransactionType === "Payment" &&
tx.metaData?.TransactionResult === "tesSUCCESS"
) {
// Construct a simplified custom payload with key transfer details
results.push({
hash: tx.hash,
ledgerIndex: ledger.ledger_index,
closeTime: ledger.close_time_iso, // Human-readable timestamp
account: tx.Account, // Sender
destination: tx.Destination, // Receiver
fee: tx.Fee
});
}
}
}
// Return the filtered results, or null to skip the ledger if no payments were found
return results.length ? results : null;
}
Example response:
[
{
"account": "rUg8ac5ikpTaWk5RPei8xuYkNEyUs53G1i",
"closeTime": "2025-12-09T21:49:31Z",
"destination": "rNxp4h8apvRis6mJf9Sh8C6iRxfrDWN7AV",
"fee": "12",
"hash": "0059A19205DA30084FB42C54C343BED150D8F3EB5443FA2C77B87F540A17D6D7",
"ledgerIndex": "100768299"
},
{
"account": "rUg8ac5ikpTaWk5RPei8xuYkNEyUs53G1i",
"closeTime": "2025-12-09T21:49:31Z",
"destination": "rMqfygR9sbZvWMRqStzUunBXH8Ut5DLfxs",
"fee": "12",
"hash": "099243DEE9C12B90A6080A99FC10EB620CDBB713BF17CC6E5D674176A4308A0A",
"ledgerIndex": "100768299"
},
// ...
]
Tips for Backfilling with Streams
Debugging Filters
Use console.log() in your filter function to debug during development. Logs appear in the Logs tab next to the Results tab in the Stream Filter Editor.
function main(payload) {
const {
data,
metadata,
} = payload;
console.log("Data:", data)
console.log("Metadata:", metadata)
// ... rest of your filter
}
Testing with Raw Data
When writing a new filter, you can download the raw data for a specific test block from the Stream configuration UI. This helps you understand the exact payload structure before writing filter logic.
Batching for Faster Backfills
By default, Streams delivers one block at a time. For historical backfills, increasing the batch size (e.g., 10 or 100 blocks per delivery) reduces overhead and speeds up ingestion. Adjust this in your Stream settings based on your destination's capacity.
Consider using Elastic Batch for automatic batch size adjustment if you continue to real-time streaming.
Cost Optimization
Combining Filtering (to remove unwanted data) with Compression (to shrink payload size) is the most effective way to manage costs during large-scale backfills.
Additional Resources
- Documentation: Streams Cost Estimation
- Documentation: Streams Pricing & Data Usage
- Technical guide: Reduce Streams Costs with Data Compression
TIP: If your filter returns
null, you are not charged for that block.
Security and Authentication
When backfilling, ensure your destination endpoints are secured. This protects your data during transit and prevents unauthorized access. Streams supports custom headers and includes a security token with each delivery that you can use to verify the request originated from Quicknode.
Additional Resources
- Technical guide: How to Validate Incoming Streams Webhook Messages
Related Resources
- Product Page and Backfill Templates: Quicknode Streams Backfills
- Video Tutorial: Blockchain Data Backfilling
- Documentation: Streams Overview
- Documentation: Streams Data Sources
- Documentation: Streams Destinations
- Documentation: Streams Filters
- Documentation: Key-Value Store Overview
- Technical guides: All Streams Guides
- Enterprise Backfilling: Contact Sales for dedicated support and custom solution for larger-scale backfills.