DEV Community

Ramprasad Edigi
Ramprasad Edigi

Posted on

Chainlink Automation Isn't a Cron Job. It's a Consensus Decision

The mental model most developers have is wrong

Ask most developers how Chainlink Automation works and they'll say something like: "A bot checks your contract on a schedule, and if checkUpkeep returns true, it calls performUpkeep." That's not wrong exactly, but it's missing the part that actually matters for security: the decision to call performUpkeep isn't made by one bot. It's made by a decentralized oracle network reaching consensus through OCR3, and the result is cryptographically signed before anything touches the chain.

That distinction isn't academic. The security properties of your automated contract depend entirely on which version of this mental model you're using when you design it.

This is day 8 of the 28-day Chainlink architecture series. Today goes through Automation's actual architecture, the three trigger types, the OCR3 consensus flow, and the audit-relevant mistakes that come from treating Automation as a simple keeper bot.

What an Upkeep actually is

An Upkeep is a registered job in the Automation network. When you register an Upkeep, you're telling the Automation Registry: "Here is a contract, here is the condition to check, here is the function to call when that condition is true, and here is the LINK balance funding the operation."

The Registry is the coordination layer. It tracks which Upkeeps exist, which node operators are registered to service them, and how much each Upkeep has paid for work already done. Node operators don't discover Upkeeps by watching the chain themselves. They query the Registry to know what to service.

Three trigger types, not one

This is where most content oversimplifies. Automation isn't just "runs on a schedule." There are three distinct trigger types, each with a different check mechanism:

Time-based triggers are the closest thing to a cron job. You specify a CRON expression and the network calls your function on that schedule. As of December 11, 2025, time-based upkeeps were updated so that only the unique Forwarder contract assigned to your specific Upkeep can call the target function, removing the ability of third parties to trigger the upkeep directly and preventing a class of attacks where an insufficiently-gated call with wrong gas parameters would tick the timer forward without successfully executing the intended action.

Custom logic triggers are the most flexible. Your contract implements AutomationCompatibleInterface, which requires two functions: checkUpkeep and performUpkeep. Nodes simulate checkUpkeep off-chain as a view function to determine eligibility, then call performUpkeep on-chain when consensus says it's time. This is where most of the security-relevant design decisions live, covered in detail below.

Log triggers are event-driven. Instead of polling a condition on every block, an Upkeep fires when a specific event is emitted by a specific contract. The Automation network monitors for the log and triggers performUpkeep when it appears. This enables reactive automation: a Uniswap swap fires an event, your Upkeep responds. Practical for arbitrage bots, liquidation watchers, and any system that needs to react to on-chain events rather than polling state.

The OCR3 consensus flow, step by step

Here's what actually happens from the moment an Upkeep becomes eligible to the moment performUpkeep executes. This is the part the "cron job" mental model completely misses.

Automation nodes form a peer-to-peer network using OCR3, the same protocol Day 4 of this series covered in the context of Data Feeds. Each node independently simulates the checkUpkeep function against its own view of the chain state. Not one node. Every node in the network, independently, running the same simulation.

When a node's simulation returns upkeepNeeded = true, it doesn't immediately send a transaction. It broadcasts that observation to the other nodes in the network. The OCR3 consensus round then proceeds: nodes share their observations, reach agreement on which Upkeeps are eligible, and sign a report. That signed report contains the performData that will be executed on-chain.

The signed report is submitted to the Registry contract. The Registry validates the report's signatures before executing anything. If the signatures don't represent a sufficient quorum of the configured node set, nothing executes. The cryptographic guarantee isn't just "nodes checked," it's "enough independent nodes agreed AND the Registry verified their agreement before touching your contract."

This is why Automation is called verifiable compute, not just automated compute. The on-chain validation step is where the decentralization claim actually gets enforced, exactly like the aggregator contract's signature check in the Data Feeds architecture.

checkUpkeep: free computation, real design constraints

checkUpkeep is a view function. It costs no gas when simulated off-chain, which is exactly why nodes can afford to simulate it on every block for every registered Upkeep without burning through LINK balances constantly. The entire point of the checkUpkeep/performUpkeep separation is to push expensive computation off-chain for free and only pay for the on-chain execution when it's actually needed.

This creates a real design pattern: do all your complex logic in checkUpkeep. Determine exactly which accounts need liquidating, which positions need rebalancing, which indexes in an array have crossed a threshold. Encode all of that as performData. Then performUpkeep receives that pre-computed result and executes only what checkUpkeep already determined was necessary.

One constraint that bites developers: checkUpkeep has a gas limit for the simulation. If your condition check runs too much computation, the simulation exceeds checkGasLimit and the Upkeep is simply not performed, with no on-chain revert to surface the issue. The failure is silent from the contract's perspective.

The audit checklist for Automation integrations

1. Is performUpkeep gated properly?

performUpkeep will be called by the Registry (or a Forwarder for time-based Upkeeps). If your performUpkeep function can be called by any address, anyone can trigger it with arbitrary performData, not just the Automation network. Either check msg.sender against the Registry address, or use Chainlink's Forwarder pattern that assigns a unique, immutable caller address to each Upkeep.

// VULNERABLE: no caller check
function performUpkeep(bytes calldata performData) external override {
    // anyone can call this
}

// SAFER: restrict to the Registry or Forwarder
function performUpkeep(bytes calldata performData) external override {
    require(msg.sender == forwarder, "Only forwarder");
    // execute
}
Enter fullscreen mode Exit fullscreen mode

2. Is performUpkeep idempotent?

If checkUpkeep returns true for a condition, multiple nodes in the network may attempt to call performUpkeep during the same eligibility window before the on-chain state updates. Your performUpkeep should check the condition again on-chain and return safely (not revert) if the work is already done. A revert inside performUpkeep doesn't just fail the transaction, it can affect how the Automation network handles retry logic.

3. Is the gas limit set high enough?

The performGasLimit is set at registration time. If performUpkeep's execution exceeds this limit, the network won't execute it because the simulation would show it failing. Unlike a regular transaction where you find out at execution time, Automation won't even attempt on-chain execution if the simulation exceeds the limit. This is a silent non-execution, not a revert you'll see on a block explorer.

4. Is the LINK balance monitored?

An Upkeep with a zero or below-minimum LINK balance simply stops being performed. There's no on-chain revert, no error, just silence. For a liquidation bot or a DeFi protocol relying on Automation for critical maintenance, running out of LINK at the wrong moment has real financial consequences. Automate the top-up or set an alert before the balance hits the minimum threshold.

5. Is checkUpkeep view-only with no state changes?

checkUpkeep is simulated off-chain as a view function. Any state changes inside checkUpkeep will not persist (it's a simulation, not an actual transaction). Code that assumes checkUpkeep writes to storage is silently wrong: the check runs, the write appears to happen in the simulation context, and then evaporates when the simulation ends.

A concrete pattern: off-chain computation, on-chain precision

Here's what the checkUpkeep/performUpkeep split looks like when used properly for something non-trivial, like rebalancing a set of vault positions when any of them drift past a threshold.

function checkUpkeep(bytes calldata)
    external view override
    returns (bool upkeepNeeded, bytes memory performData)
{
    uint256[] memory needsRebalance = new uint256[](positions.length);
    uint256 count = 0;

    for (uint256 i = 0; i < positions.length; i++) {
        if (_isDrifted(positions[i])) {
            needsRebalance[count] = i;
            count++;
        }
    }

    if (count > 0) {
        upkeepNeeded = true;
        performData = abi.encode(needsRebalance, count);
    }
}

function performUpkeep(bytes calldata performData) external override {
    require(msg.sender == forwarder, "Only forwarder");
    (uint256[] memory indexes, uint256 count) =
        abi.decode(performData, (uint256[], uint256));

    for (uint256 i = 0; i < count; i++) {
        _rebalance(positions[indexes[i]]);
    }
}
Enter fullscreen mode Exit fullscreen mode

checkUpkeep loops over potentially hundreds of positions, doing all the math off-chain at zero gas cost. It encodes exactly which positions need work and passes that as performData. performUpkeep receives that pre-computed list and only touches the positions already identified as drifted, no wasted computation on-chain, no looping through the full array at gas cost.

This pattern is what separates "Automation as a cron job" from Automation as actual verifiable off-chain compute.

Chainlink Automation is not a bot that calls your function. It's an OCR3-based oracle network that reaches cryptographically verified consensus on whether your condition is true, signs a report proving that consensus, and only then delivers the call to your contract. Design checkUpkeep to do the heavy lifting off-chain for free, design performUpkeep to be gated, idempotent, and gas-bounded, and monitor your LINK balance like you monitor any other critical infrastructure.


I'm a smart contract security researcher writing through Chainlink's full architecture for 28 days. Follow along at ramprasadgoud.dev or on X @0xramprasad.

Top comments (0)