Chainlink Data Feeds are decentralized oracle networks that aggregate data from multiple independent sources and publish the result onchain, where any contract can read the latest value in a single call. This article explains the push-based feed model, walks through a runnable Solidity consumer using AggregatorV3Interface, and shows how to add staleness and L2 sequencer-uptime guards that prevent your protocol from acting on bad data. It also covers offchain reads, a reproducible verification procedure, and the tradeoffs between Data Feeds and pull-based Data Streams.
What Chainlink Data Feeds actually deliver
Chainlink Data Feeds are onchain data products produced by decentralized oracle networks (DONs). Each feed aggregates values from multiple independent data providers and node operators, then publishes a single validated answer to an onchain aggregator contract. Your smart contract reads that answer through a proxy address, so the underlying implementation can be upgraded without changing your integration.
The most common feed type is a price feed (for example, an asset quoted in USD), but the same delivery model also carries proof-of-reserves data, net asset values, interest-rate curves, and multi-variable bundles. The key property is that the data is already onchain: reading it is a normal contract call, not an API request, and it costs only the gas of that call.
This article focuses on the integration mechanics that matter in production: how the push-based model behaves, how to read a feed correctly, how to detect stale answers, and how to verify your integration reproducibly. It does not attempt to enumerate every feed or network; use the official feed directory for that inventory.
- Push-based: the DON writes updates onchain on a heartbeat and when the value deviates beyond a threshold.
- Proxy pattern: you call a stable proxy address; the aggregator behind it can be replaced.
- Single-call reads: no offchain request, no callback, no additional trust assumption at read time.
Assumptions and prerequisites
The examples assume an EVM-compatible chain, a Solidity toolchain (Foundry or Hardhat), and access to an RPC endpoint for the target network. You will need the proxy address of the specific feed you intend to read; addresses are network-specific and must be taken from the official Chainlink feed directory rather than copied from another chain.
For offchain reads you will need Node.js with ethers or viem, or Python with web3.py. For any script that sends transactions you will need a funded account; keep the private key in an environment variable and never commit it. If you are still choosing an RPC provider for your target chain, the RPC Assistant can help you pick a concrete endpoint, and the multi-chain RPC endpoints guide explains how to structure access across networks.
The push-based feed model and its update triggers
A Data Feed does not update on every price tick. The DON publishes a new round when either a heartbeat interval elapses or the observed value moves beyond a configured deviation threshold. Each update creates a new round with an ID, an answer, and an update timestamp, and the aggregator exposes the latest round through a standard interface.
This design has two consequences you must handle explicitly. First, the answer can be older than the current market price by up to the heartbeat interval, so any logic that depends on freshness needs a staleness check. Second, because updates are event-driven, the time between rounds is not constant; a quiet market may produce long gaps even though the feed is healthy.
The interface your contract consumes is AggregatorV3Interface, which exposes latestRoundData, getRoundData, decimals, and description. The decimals value is per feed and is not always 8, so never hardcode it.
Reading a feed from a Solidity contract
The consumer below reads the latest round, validates that the answer is positive and fresh, and normalizes the value to 18 decimals. Replace FEED_ADDRESS with the proxy address for your network and asset, and set MAX_STALENESS to a value derived from the feed's documented heartbeat rather than a guess.
The check on answeredInRound is a defensive measure: in some historical aggregator implementations it could lag behind roundId, and treating that as stale avoids acting on an incomplete round.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface AggregatorV3Interface {
function decimals() external view returns (uint8);
function description() external view returns (string memory);
function latestRoundData()
external
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
}
contract FeedReader {
AggregatorV3Interface public immutable feed;
uint256 public immutable maxStaleness;
error StaleAnswer(uint256 updatedAt);
error NonPositiveAnswer(int256 answer);
constructor(address feedAddress, uint256 maxStalenessSeconds) {
feed = AggregatorV3Interface(feedAddress);
maxStaleness = maxStalenessSeconds;
}
function latestPrice18() external view returns (uint256) {
(uint80 roundId, int256 answer, , uint256 updatedAt, uint80 answeredInRound) = feed.latestRoundData();
if (answer <= 0) revert NonPositiveAnswer(answer);
if (updatedAt == 0 || block.timestamp - updatedAt > maxStaleness) revert StaleAnswer(updatedAt);
if (answeredInRound < roundId) revert StaleAnswer(updatedAt);
uint8 d = feed.decimals();
return uint256(answer) * (10 ** (18 - d));
}
}Guarding L2 deployments with sequencer uptime feeds
On Layer 2 networks, a feed can appear fresh while the sequencer has only recently come back online, because transactions queued during an outage may execute in a burst. Chainlink publishes L2 Sequencer Uptime Feeds for this case. The pattern is to read the uptime feed, confirm the sequencer has been up for a grace period, and only then trust the price feed.
The grace period exists because prices may not have been refreshed immediately after the sequencer resumed. Choose a grace period that reflects how quickly your target feed updates after an outage, and document the choice. The same guard should wrap every price read in the affected code path, not just the entry point.
interface AggregatorV3Interface {
function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80);
}
contract SequencerGuard {
AggregatorV3Interface public immutable uptimeFeed;
uint256 public constant GRACE_PERIOD = 30 minutes;
constructor(address uptimeFeedAddress) {
uptimeFeed = AggregatorV3Interface(uptimeFeedAddress);
}
function sequencerHealthy() public view returns (bool) {
(, int256 answer, uint256 startedAt, , ) = uptimeFeed.latestRoundData();
if (answer != 0) return false; // 0 = up, 1 = down
return block.timestamp - startedAt > GRACE_PERIOD;
}
}Reading feeds offchain with ethers
Offchain reads are useful for dashboards, liquidation bots, and pre-trade checks. The script below reads the latest round and prints the normalized price, the round ID, and the age of the answer. Set RPC_URL to your provider endpoint and FEED_ADDRESS to the proxy for your network.
Because the read is a plain eth_call, it works against any standard RPC endpoint. If you are comparing providers for latency or archive depth, the Ethereum RPC API overview and the pricing page describe what to evaluate.
import { ethers } from "ethers";
const ABI = [
"function decimals() view returns (uint8)",
"function latestRoundData() view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)"
];
async function main() {
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
const feed = new ethers.Contract(process.env.FEED_ADDRESS, ABI, provider);
const [decimals, round] = await Promise.all([
feed.decimals(),
feed.latestRoundData()
]);
const price = Number(round.answer) / 10 ** Number(decimals);
const ageSeconds = Math.floor(Date.now() / 1000) - Number(round.updatedAt);
console.log({ price, roundId: round.roundId.toString(), ageSeconds });
}
main().catch((e) => { console.error(e); process.exit(1); });A reproducible verification procedure
Before trusting an integration, verify it against the feed's own metadata rather than against a price you saw elsewhere. The procedure below produces a small results table you can re-run on any network and compare over time. It does not require any specific numeric outcome; the point is that the columns are populated and internally consistent.
Run the offchain script three times spaced a few minutes apart, and record the following columns in a Markdown table: network, feed address, description, decimals, roundId, answer, updatedAt, age in seconds, and whether age is below your staleness threshold. A healthy feed shows a monotonically non-decreasing roundId, a positive answer, and an age below the heartbeat plus a small margin. If roundId does not advance across runs, that is expected in a quiet market; it is not by itself a fault.
Then call your Solidity consumer on a fork or testnet and confirm that latestPrice18 returns the same normalized value as the offchain script. A mismatch usually indicates a decimals assumption error or a different feed address than intended.
- Confirm description matches the asset pair you expect.
- Confirm decimals matches the value you used to normalize.
- Confirm updatedAt is non-zero and age is within your threshold.
- Confirm the onchain consumer and offchain script agree.
Troubleshooting common integration failures
Most Data Feed integration bugs fall into a small set of categories. The table below maps symptoms to likely causes and the check that confirms each one.
If a read reverts with no data returned, the address is probably not a contract on that chain, which usually means the feed address was copied from a different network. If the price is off by orders of magnitude, the decimals value was hardcoded or applied twice. If your protocol behaves oddly only on an L2, check whether the sequencer guard is present and whether the grace period is long enough for your feed.
- Revert on call: verify the address is a contract on the target chain using eth_getCode.
- Price scaled incorrectly: read decimals() and normalize once.
- Stale data accepted: add an updatedAt check tied to the documented heartbeat.
- L2 burst after outage: add a sequencer uptime guard with a grace period.
- Round mismatch: compare answeredInRound with roundId.
Limitations and tradeoffs
Push-based Data Feeds optimize for onchain composability and predictable read costs, not for minimum latency. If your application needs sub-second updates or per-block granularity, pull-based Data Streams are the appropriate product, but they require a different verification flow and are not a drop-in replacement for a latestRoundData call.
Heartbeat and deviation thresholds are per feed and can change; a staleness constant that is correct today may be wrong after a feed configuration update. Treat the threshold as configuration, monitor it, and alert when the observed age approaches it. Also remember that a feed's answer is an aggregate, not a guarantee about any single venue; for collateral valuation, combine it with your own risk parameters rather than treating it as ground truth.
Finally, the RPC layer matters for offchain reads. A lagging node can return an older block and therefore an older round. If freshness is critical, read at a specific block number and compare across providers, and consider a dedicated node for consistent head latency.
Next steps for a production integration
Start by listing every code path that consumes a price and confirm each one has a staleness check and, on L2, a sequencer guard. Then add monitoring that records roundId and age on a schedule so you can see feed behavior before an incident. If you operate your own infrastructure, a dedicated node reduces variance in offchain read latency; the dedicated node overview explains the tradeoffs.
For teams building across several chains, standardize feed addresses and decimals in a configuration file per network and load it at deploy time rather than hardcoding. The networks directory lists supported chains, and the RPC pricing page helps you size endpoint capacity for the read volume your monitors generate. When you are ready to choose a concrete endpoint for a specific chain, ask the RPC Assistant for a recommendation rather than guessing.