New BoltRPC, our weighted-unit developer RPC with unlimited RPS, live on 35+ chains
Chainlink

How Chainlink Price Feeds Work: Deviation and Heartbeat

How Chainlink price feeds update: nodes agree on a median off-chain, and one signed report lands on a deviation threshold or heartbeat. Code for stale prices.

11 min read

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 check updatedAt against 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:

  1. The nodes talk to each other over a peer-to-peer network and regularly elect a leader.
  2. The leader asks the other nodes (the followers) for freshly signed observations and combines them into a report.
  3. If a quorum of followers approves the report, the leader attaches the quorum’s signatures and sends the final report to all followers.
  4. 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.
  5. 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 (updatedAt 1792703747), with an answer of 2391.70345062.
  • The next one arrived at 22:16:23 UTC (updatedAt 1792707383), 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 round 129127208515966896220 is phase 7, aggregator round 34908.
  • answer: the price as an integer without a decimal point. Divide by 10^decimals to 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, startedAt was 1792707363 and updatedAt was 1792707383, 20 seconds apart.
  • answeredInRound: deprecated according to the documentation. On both feeds we read it equals roundId.

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:

  1. Fresh. The call returned 239170345062, 8 decimals and updatedAt 1792703747. The age was 3315 seconds, under the 3900 limit.
  2. Stale. We advanced the fork’s clock by 7200 seconds. The same call reverted with StalePrice(1792703747, 10544, 3900).
  3. 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.


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.

Frequently asked

Questions & answers

What is a Chainlink deviation threshold?

The deviation threshold is the percentage by which the off-chain price must differ from the price stored on-chain before the nodes write a new round. For ETH/USD on Ethereum mainnet it was 0.5% on 22 October 2026. For USDC/USD it was 0.25%. Each feed has its own value, so read it from the feed's entry in Chainlink's price feed addresses list.

What is a Chainlink heartbeat?

The heartbeat is the longest time a feed may go without an update. If the price has not moved past the deviation threshold when the heartbeat runs out, the nodes write a new round anyway, so the on-chain answer is normally not older than the heartbeat plus a short delay. Chainlink's documentation notes that congestion can delay an update. ETH/USD on Ethereum had a 3600 second heartbeat on 22 October 2026, and 196 of the 221 price feed entries in Chainlink's reference data for Ethereum mainnet (including SVR variants and feeds marked for deprecation) had a 24 hour heartbeat.

How often does a Chainlink price feed update?

There is no fixed schedule. A feed updates when the price moves past its deviation threshold or when its heartbeat runs out, whichever happens first. Over 48 hours ending on 22 October 2026, the ETH/USD feed on Ethereum mainnet wrote 60 reports, 26 of them after a price move and 33 on the heartbeat, with a median gap of 3612 seconds between reports.

How do I check if a Chainlink price is stale?

Read latestRoundData() on the feed's proxy contract, subtract updatedAt from the current block timestamp and revert or pause if the result is larger than a limit you set. Set the limit per feed to its heartbeat plus a margin, because a healthy 24 hour feed can be 20 hours old while a 1 hour feed needs a limit just above one hour. On L2 networks also check the Sequencer Uptime Feed.

How many nodes update a Chainlink price feed?

It varies by feed. On 22 October 2026 the ETH/USD feed on Ethereum mainnet had 31 configured transmitters and USDC/USD had 16. Reports on ETH/USD carried 30 or 31 node observations, and the feed is configured to tolerate 10 faulty nodes out of 31.

What does latestRoundData return?

It returns five values: roundId, answer, startedAt, updatedAt and answeredInRound. The answer is an integer scaled by the feed's decimals (8 for ETH/USD, so 239170345062 means 2391.70345062). updatedAt is the block timestamp of the report that set the answer. answeredInRound is deprecated.

Get in touch

Ready to build on reliable blockchain infrastructure?

Whether you are a financial institution tokenizing assets, an asset manager launching on-chain products, a custody provider expanding into staking, or a Web3 protocol scaling oracle and validator operations, our team is ready to help.

Trusted by
ChainlinkEnjinPolygondRPC