A Chainlink price feed is a smart contract that stores one price and is kept current by a network of independent nodes. Each node reads the price from several data providers. The nodes then agree on a median off-chain, using Chainlink’s Off-Chain Reporting (OCR) protocol, and one signed report is written on-chain. That write happens when the price moves past the feed’s deviation threshold or when its heartbeat runs out, whichever comes first.
Those two triggers decide how fresh a price is and what your application has to check. This guide walks through the full path from data provider to contract, shows real numbers read from the ETH/USD feed on Ethereum mainnet, and gives tested code to read a feed and refuse a stale price.
The short version
- Each node queries several data providers and signs its own observation of the price.
- A leader node collects signed observations, builds a report and gets a quorum of nodes to sign it. One node transmits it to the aggregator contract, which verifies the signatures and stores the median.
- A new round is written when the price deviates past a set percentage or when the heartbeat time has passed since the last update.
- The values differ per feed. ETH/USD on Ethereum had a 0.5% deviation threshold and a 1 hour heartbeat on 22 October 2026. Most Ethereum mainnet price feeds use a 24 hour heartbeat.
- Your contract reads the feed’s proxy with
latestRoundData()and must checkupdatedAtagainst that feed’s heartbeat.
All numbers marked 22 October 2026 are UTC reads from Ethereum mainnet over public RPC endpoints. Deviation and heartbeat values come from Chainlink’s reference data for price feeds. Other chains and other feeds differ, and these values can change.
From data providers to a signed report
Chainlink’s documentation describes three layers of aggregation in a market price feed: the data source, the node operator and the oracle network. The first two happen before anything reaches the chain.
Data providers collect market data from exchanges and publish aggregated prices. For crypto prices, Chainlink’s data source rules call for three or more crypto price data aggregators or market data vendors using centralized or decentralized exchange data. Each node operator queries several of these sources and combines the answers into a single observation. Providers are a separate role from node operators, which what is a Chainlink data provider covers in detail.
Then the nodes run an OCR round:
- The nodes talk to each other over a peer-to-peer network and regularly elect a leader.
- The leader asks the other nodes (the followers) for freshly signed observations and combines them into a report.
- If a quorum of followers approves the report, the leader attaches the quorum’s signatures and sends the final report to all followers.
- The nodes try to transmit the final report to the aggregator contract on a randomized schedule. If the designated node does not get its transaction confirmed in time, a round-robin protocol lets other nodes transmit until one succeeds.
- The aggregator contract verifies that a quorum signed the report and exposes the median value as the answer, with a block timestamp and a round ID.
The reason for this design is cost and timeliness. Only one transaction is submitted per round instead of one per node, which reduces congestion and gas spend and lets feeds update faster.
What the contract checks
The word “quorum” has a precise meaning in the contract. On an OCR2 aggregator the contract requires exactly f + 1 signatures, where f is the number of faulty nodes the feed is configured to tolerate. It also requires 3f to be smaller than the number of signers, and it rejects a report with f or fewer observations. Off-chain, the contract’s own comments say the nodes work from a matching set of at least 2f + 1 observations.
ETH/USD on Ethereum mainnet shows what that means in numbers. Its current configuration was set in block 25557130 on 18 July 2026, with 31 signers and f = 10. So the contract checks 11 signatures, the off-chain quorum is a matching set of at least 21 observations, and the feed tolerates 10 faulty nodes. We decoded this from the ConfigSet event of that block; the transaction hash is in the sources.
The answer is the median
The stored answer is the middle value of the sorted node observations. The contract code reads report.observations[report.observations.length/2] and checks it against its minimum and maximum. We confirmed this against real reports: in all 60 reports from the 48 hours ending 22 October 2026, the stored answer equalled the median of the node observations. Those reports carried 30 or 31 observations each, so a report still lands when one node is missing. Across the 60 reports, 28 different nodes did the transmitting. A median ignores a minority of outlier observations, so attackers target the market price itself instead: see how oracle manipulation attacks work.
Deviation threshold and heartbeat
A feed does not stream. Chainlink’s documentation says the aggregator updates its answer when the value deviates beyond a specified threshold or when the heartbeat idle time has passed. The first condition met triggers the update.
- Deviation threshold. A new round starts when a node sees that the off-chain value differs from the on-chain value by more than the threshold. The nodes monitor their data providers for this.
- Heartbeat. A new round starts after a set amount of time since the last update, even if the price has barely moved.
Both values are set per feed and can differ for the same asset on different chains. The documentation also notes that feeds for wrapped or liquid staking assets can have different values from the underlying asset.
Two feeds, two very different settings
On 22 October 2026, Chainlink’s reference data listed these values on Ethereum mainnet:
- ETH/USD: 0.5% deviation threshold, 3600 second (1 hour) heartbeat, 31 configured transmitters.
- USDC/USD: 0.25% deviation threshold, 82800 second (23 hour) heartbeat, 16 configured transmitters.
USDC/USD has the tighter threshold and the much longer heartbeat. The full list for each chain and each feed is on Chainlink’s price feed addresses page. A list of feeds by network is on the feeds page.
A 1 hour heartbeat is not the norm. Chainlink’s reference data for Ethereum mainnet held 221 price feed entries on 22 October 2026, including SVR variants and feeds marked for deprecation. Of those, 196 had a 24 hour heartbeat and 20 had a 1 hour heartbeat. The remaining five were 4 feeds at 23 hours and 1 at 6 hours. Treat any assumption that a heartbeat is a matter of minutes as wrong until you have read the feed’s own value. For the spread of these settings across every published feed, by chain and asset class, see Chainlink data feeds by the numbers.
What 48 hours of ETH/USD looked like
To see the two triggers at work, we read every NewTransmission event from the ETH/USD aggregator over the last 14,400 blocks (about 48 hours). It held 60 reports: the first came at 22:08:11 UTC on 20 October 2026 and the last at 21:15:47 UTC on 22 October 2026.
The first report has no earlier one to compare with, so the 59 gaps between reports split like this:
- 26 followed a move of 0.5% or more since the previous answer (deviation).
- 33 were heartbeat updates: a gap of at least 3540 seconds with a move under 0.5%.
- None were left unexplained.
One report matched both triggers, with a gap of exactly 3600 seconds and a move of 0.521%. It is counted as a deviation update.
The gaps between reports ranged from 480 seconds to 3636 seconds, with a median of 3612 seconds. That median sits just above the 3600 second heartbeat, which fits the documentation’s note that a heartbeat update can arrive with a slight delay depending on congestion. One caveat: we classified each report by comparing consecutive answers, not by reading node logs.
A heartbeat update, caught live
We also watched the feed to catch one heartbeat update as it happened:
- The previous update was at 21:15:47 UTC on 22 October 2026 (
updatedAt1792703747), with an answer of 2391.70345062. - The next one arrived at 22:16:23 UTC (
updatedAt1792707383), 3636 seconds later, with an answer of 2387.84566728. - The price had moved down 0.16%, well under the 0.5% threshold, so the heartbeat fired.
- The round ID went from 129127208515966896219 to 129127208515966896220.
The 36 seconds beyond 3600 is the kind of slight delay the documentation describes. For your application it is a reason to add a margin to any freshness limit, which the stale price section below covers.
Reading a feed: latestRoundData, decimals and the proxy
You do not read the aggregator directly. Consumers read a proxy contract, which points to the current aggregator. This lets Chainlink replace the aggregator without breaking integrations, and the documentation recommends using the proxy for exactly that reason. The ETH/USD proxy on Ethereum is 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419. On 22 October 2026 it pointed to the aggregator 0x7d4E742018fb52E48b08BE73d041C18B21de6Fb5 in phase 7. The proxy’s phase counter shows the aggregator behind that address has been swapped before, while consumers kept the same address.
latestRoundData() returns five values:
roundId: on a proxy it combines the phase and the aggregator’s round number as(phaseId << 64) | aggregatorRoundId. The round129127208515966896220is phase 7, aggregator round 34908.answer: the price as an integer without a decimal point. Divide by10^decimalsto get the real value.startedAt: in the OCR2 contract code, the time the observations were made off-chain.updatedAt: the timestamp of the block in which the report was transmitted. This is the value you check for freshness. In the heartbeat capture above,startedAtwas 1792707363 andupdatedAtwas 1792707383, 20 seconds apart.answeredInRound: deprecated according to the documentation. On both feeds we read it equalsroundId.
Read the precision from the feed with decimals() rather than hardcoding it. Crypto prices quoted in USD generally use 8 decimals and crypto prices quoted in ETH use 18, but the documentation says to read each feed’s own value. So 239170345062 from a feed with 8 decimals is 2391.70345062.
Minimum and maximum answer bounds are not a safety net either. The documentation says that on most feeds the minAnswer and maxAnswer values are no longer used and do not stop an application from reading the latest answer. On the ETH/USD aggregator, minAnswer() returns 1. Some feeds still bound their answers: on 23 October 2026 the USDC/USD aggregator had a maxAnswer of 105000000, which at 8 decimals is a cap of 1.05 dollars.
Read a feed with viem
This script reads a feed with viem 2.57.10 on Node 24.19.0. Only the RPC URL and the feed address change per chain and per feed. We ran it against Ethereum mainnet on 22 October 2026:
// Reads a Chainlink price feed with viem. Only the RPC URL and the feed address change per chain/feed.
import { createPublicClient, http, parseAbi, formatUnits } from 'viem';
import { mainnet } from 'viem/chains';
const RPC_URL = process.env.RPC_URL ?? 'https://ethereum-rpc.publicnode.com';
const FEED = process.env.FEED ?? '0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419'; // ETH / USD proxy, Ethereum mainnet
const HEARTBEAT_S = Number(process.env.HEARTBEAT_S ?? 3600); // from the feed's page, then add a buffer
const abi = parseAbi([
'function decimals() view returns (uint8)',
'function description() view returns (string)',
'function latestRoundData() view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)',
]);
const client = createPublicClient({ chain: mainnet, transport: http(RPC_URL) });
const [decimals, description, round, block] = await Promise.all([
client.readContract({ address: FEED, abi, functionName: 'decimals' }),
client.readContract({ address: FEED, abi, functionName: 'description' }),
client.readContract({ address: FEED, abi, functionName: 'latestRoundData' }),
client.getBlock(),
]);
const [roundId, answer, startedAt, updatedAt, answeredInRound] = round;
const ageS = Number(block.timestamp - updatedAt);
console.log(description, 'decimals', decimals);
console.log({ roundId: roundId.toString(), answer: answer.toString(), startedAt: startedAt.toString(), updatedAt: updatedAt.toString(), answeredInRound: answeredInRound.toString() });
console.log('price ', formatUnits(answer, decimals));
console.log('age (s) ', ageS, ageS > HEARTBEAT_S ? 'STALE (older than heartbeat)' : 'within heartbeat');
The output:
ETH / USD decimals 8
{
roundId: '129127208515966896219',
answer: '239170345062',
startedAt: '1792703732',
updatedAt: '1792703747',
answeredInRound: '129127208515966896219'
}
price 2391.70345062
age (s) 3432 within heartbeat
Setting FEED=0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6 and HEARTBEAT_S=82800 reads USDC/USD instead. That read returned a price of 0.99981853 with an age of 73728 seconds, about 20.5 hours. That is old for a price but healthy for a feed with a 23 hour heartbeat. Pointing the script at an address with no contract fails with a “returned no data” error, which is the behaviour you want.
If you use ethers, use v6 syntax: import { JsonRpcProvider, Contract, formatUnits } from 'ethers' and new JsonRpcProvider(url). Chainlink’s documentation sample still uses the v5 form new ethers.providers.JsonRpcProvider(...), which throws a TypeError on ethers 6.17.0.
Checking for stale prices
Chainlink’s documentation tells applications to track updatedAt and to pause or switch to another mode if the answer is not updated within the heartbeat or within limits the application finds acceptable. It also warns that some heartbeats last several hours. It puts the responsibility on the developer: data quality checks, circuit breakers and contingency logic are yours to build.
This contract does that for one feed. We compiled it with Foundry 1.8.6 and solc 0.8.24 against AggregatorV3Interface from the contracts-v1.3.0 tag of Chainlink’s repository:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
/// Reads one Chainlink price feed and refuses to return an answer that is
/// older than maxAge seconds. Set maxAge to the feed's heartbeat plus a buffer.
contract PriceConsumer {
AggregatorV3Interface public immutable feed;
uint256 public immutable maxAge;
error InvalidAnswer(int256 answer);
error IncompleteRound();
error StalePrice(uint256 updatedAt, uint256 age, uint256 maxAge);
constructor(address feedProxy, uint256 maxAgeSeconds) {
feed = AggregatorV3Interface(feedProxy);
maxAge = maxAgeSeconds;
}
/// @return price the answer as returned by the feed (scaled by 10**decimals)
/// @return decimals the feed's decimals, read from the same contract
/// @return updatedAt block timestamp of the report that set this answer
function latestPrice() external view returns (uint256 price, uint8 decimals, uint256 updatedAt) {
(, int256 answer, , uint256 updatedAt_, ) = feed.latestRoundData();
if (answer <= 0) revert InvalidAnswer(answer);
if (updatedAt_ == 0) revert IncompleteRound();
uint256 age = block.timestamp - updatedAt_;
if (age > maxAge) revert StalePrice(updatedAt_, age, maxAge);
// safe: answer > 0 was checked above
// forge-lint: disable-next-line(unsafe-typecast)
return (uint256(answer), feed.decimals(), updatedAt_);
}
}
We tested it on a local fork of Ethereum mainnet with the ETH/USD proxy and a maxAge of 3900 seconds, which is the 3600 second heartbeat plus a 300 second buffer:
- Fresh. The call returned 239170345062, 8 decimals and
updatedAt1792703747. The age was 3315 seconds, under the 3900 limit. - Stale. We advanced the fork’s clock by 7200 seconds. The same call reverted with
StalePrice(1792703747, 10544, 3900). - Control. A second deployment with a one-year
maxAge, on the same chain state, returned the price. So the revert in step 2 came from the age check.
The ages in steps 1 and 2 depend on the moment of the run, so read them as an example. A later run gives different numbers, but the same pattern: a pass inside the limit and a StalePrice revert once the clock moves past it.
The InvalidAnswer and IncompleteRound branches were not exercised, because we did not build a mock feed. The sample also has no L2 check. On L2 networks such as Arbitrum, OP and Metis, Chainlink’s documentation says you must also check the L2 Sequencer Uptime Feed.
Set the limit per feed
A single global maxAge is a bug. The limit has to follow the feed’s heartbeat:
- A 3900 second limit suits ETH/USD, with its 3600 second heartbeat.
- The same 3900 seconds would reject USDC/USD almost all the time, because that feed was 73728 seconds old while perfectly healthy. Its limit has to sit above 82800 seconds.
- Add a margin above the heartbeat for the delay seen in the live capture, where the update came 36 seconds late.
Remember what a fresh timestamp does not tell you. Inside its heartbeat, the stored price can lag the market until the move passes the deviation threshold, which is 0.5% on ETH/USD. If your application needs a fresher price than the threshold allows, that is a reason to look at a pull-based design, covered at the end.
What node operators do
Every price a feed publishes depends on node operators doing the same few jobs, continuously:
- Run the node. The node takes part in the peer-to-peer OCR protocol, including leader elections, and may be asked to transmit a report on the randomized schedule.
- Connect to data providers. The node queries several sources for each feed it supports and produces its own observation.
- Sign reports. The node signs its observation and, in a round, approves or rejects the leader’s report.
- Stay online. Each operator is rewarded for publishing data, and the feed depends on enough nodes being reachable.
The configuration tolerates failures within limits. On ETH/USD that is 10 faulty nodes out of 31. Chainlink’s OCR3 research paper states that any f < n/3 oracles may be Byzantine-faulty. It adds that if faulty and offline nodes together reach n/3, the protocol may lose liveness but must always keep its safety properties. For a feed, losing liveness would mean it stops updating, not that it accepts a wrong value. Over the 48 hour window, the largest absence we saw was a single node.
The node count is also not a fixed property of Chainlink. It varies from feed to feed, and it changes when a feed’s committee is reconfigured. For how to judge an operator’s track record, see how to evaluate a Chainlink node operator and what is a Chainlink node operator.
Push feeds and pull
Price feeds are push-based: the nodes write to the chain when a trigger fires, and your contract reads the stored value. Chainlink Data Streams uses a pull-based design, where low-latency data is delivered off-chain and verified on-chain when your application needs it. The tradeoff is the one covered above: a push feed is always as old as its last trigger, up to the heartbeat.
For the wider picture of how oracles bring off-chain data on-chain, see what is a blockchain oracle, and for the network as a whole, what is Chainlink.
Sources & References
Documentation pages were opened on 22 and 23 October 2026. On-chain values were read over public RPC endpoints on the same day.
- Chainlink Documentation, Decentralized Data Model
- Chainlink Documentation, Offchain Reporting
- Chainlink Documentation, Data Feeds overview
- Chainlink Documentation, Price Feeds
- Chainlink Documentation, Price Feed Addresses
- Chainlink Documentation, Using Data Feeds (the ethers v5 sample)
- Chainlink Documentation, Data Feeds API Reference
- Chainlink Documentation, Selecting Data Feeds
- Chainlink Documentation, Data Sources
- Chainlink Documentation, Developer Responsibilities
- Chainlink Documentation, Getting Started with Data Feeds
- Chainlink Documentation, Data Streams Architecture
- Chainlink Reference Data Directory, Ethereum mainnet feeds (deviation threshold, heartbeat and decimals per feed)
- libocr, OCR2Aggregator.sol (median, signature and observation rules)
- Chainlink, AggregatorProxy.sol (round ID encoding)
- Chainlink, AggregatorV3Interface.sol, tag contracts-v1.3.0
- Chainlink Labs Research, OCR3 protocol paper
- Chainlink Blog, How Chainlink Price Feeds Secure the DeFi Ecosystem (2022; used for the node-level median concept only)
- Ethereum mainnet reads, 22 October 2026: ETH/USD proxy
0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419, aggregator0x7d4E742018fb52E48b08BE73d041C18B21de6Fb5; USDC/USD proxy0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6, aggregator0x54bCC589d9743E521c64233706FC8cB36D275b07. Configuration (f = 10, 31 signers) set in block 25557130, transaction0x7bd492122d3aea019edfc31bb57c742b1c4b1f0d0fb8f1087cda439d8b379a7d.
Work with Matrixed.Link
Matrixed.Link operates Chainlink oracle infrastructure, validator nodes and full-stack blockchain infrastructure for protocols and institutions that need institutional-grade reliability. Official Chainlink node operator running Data Feeds, CRE, SVR and Proof of Reserve. ISO/IEC 27001:2022 certified. AAA-rated provider on StakingRewards.
Contact Matrixed.Link to discuss Chainlink oracle infrastructure for your protocol.