Chainlink’s own wrapper contract on Base asked for 2,451,496 gas to run a callback. The callback needed 2,451,381. It left 115 gas of margin, which is 0.005% of the request, and the draw landed.
Nobody else on the chain is playing that game. Over the seven days to 19 October 2026 I measured every randomness callback delivered on Base: what each game contract asked for, and what its callback actually consumed. Fifty-six of the fifty-seven consumer contracts use one fixed number for every request they have ever made, and fifty-five of those numbers are a multiple of 10,000. They are not measurements. They are guesses, rounded.
TL;DR
- Across 12,725 fulfilled Base draws, contracts reserved 6,171,681,134 gas of callback allowance and consumed 2,878,319,773. That is 46.64%, or 2.14 times more allowance than work.
- 57 consumer contracts requested randomness. 56 of them send one fixed
callbackGasLimiton every request; 55 of those are multiples of 10,000. The twelve distinct values include 200,000 (21 contracts), 500,000 (13) and 2,500,000, which is the chain maximum (4). - The 57th is Chainlink’s own
VRFV2PlusWrapper, which computes the limit per request. Its 214 draws ran at 100.0% to 101.4% of what they asked for, and its tightest left 115 gas unused. - A bigger limit does not make the draw more expensive. Two contracts with near-identical callbacks (11,364 and 12,548 gas) and limits 5 times apart paid median fees within 3% of each other. The limit sets your reserve, not your bill.
- The tail is real and unmeasured: one contract’s worst callback was 2.91 times its own median, another’s was 8.21 times. Two draws ran above 82% of a round number that was never chosen with them in mind.
- Getting it wrong downward is unrecoverable. A callback that runs out of gas is charged, recorded as
success = false, and never retried.
The number that decides whether your draw arrives
When a game contract asks Chainlink for randomness, it names a callbackGasLimit: the gas the oracle will make available when it comes back to deliver the words. The coordinator calls your contract with exactly that much and no more. If your callback needs more, it reverts, the coordinator catches the revert, and you are billed anyway. I measured that failure mode directly in 13 Base Draws Were Delivered, Charged and Refused: the request commitment is cleared whether or not your code survived, so there is no retry and no refund.
The number does a second job. Multiplied by your gas lane’s maximum price and the premium, it sets the minimum balance your subscription must hold before the oracle will answer at all. That is the mechanism behind 131 Base Subscriptions Cannot Be Closed, where five requests this week sat unanswered because the balance fell under the reserve their own limit demanded.
So one integer governs whether your randomness is delivered and how much capital you must park to be eligible for it. On Base, 55 contracts set it to a round number and never touched it again.
How I measured what the callbacks actually needed
The coordinator derives each delivered word as keccak256(abi.encode(outputSeed, i)), and it emits outputSeed in the RandomWordsFulfilled log. That means every callback ever made on Base can be rebuilt from logs alone, which is what I did in 34,029 VRF Words Passed Every Test.
For each of the 12,725 fulfilled draws in the window I rebuilt rawFulfillRandomWords(uint256,uint256[]) with the real request id and the real delivered words, then ran eth_estimateGas against the consumer contract, from the coordinator’s address, pinned to the block before the real fulfilment. Subtracting the transaction’s intrinsic cost (21,000 plus calldata) gives the gas the callback itself needed. Three controls:
- The same call from a junk address reverts. Consumers gate
rawFulfillRandomWordson the coordinator, so a reply from0x...dEaDis refused. If my probe were measuring a no-op, the control would pass. - The estimator’s bias is small and measured, not assumed.
eth_estimateGasbinary-searches for a limit that works, so it can overshoot. I binary-searched the exact threshold by hand on nine draws, callingeth_callwith explicit gas caps until success flipped to failure. The overshoot ranged from 0.12% to 2.27%. Every utilisation figure below is therefore slightly generous to the contract being measured. - One contract’s limit is known to be exact, and the method says so. Chainlink’s wrapper computes its limit from the caller’s request plus its own overhead. If my measurement were systematically wrong, the wrapper would not read as 100%. It does.
The window is Base blocks 52,520,200 to 52,822,599, seven days ending 19 October 2026 at 18:29 UTC, pulled in 1,000-block spans from an archive endpoint: 12,730 requests, 12,725 fulfilled, five still outstanding. Coordinator at 0xd5d517abe5cf79b7e95ec98db0f0277788aff634.
Twelve numbers, and eleven of them are round
Here is every fixed callbackGasLimit in use on Base, with the number of contracts that chose it.
| Limit | Contracts | Comment |
|---|---|---|
| 40,000 | 1 | |
| 100,000 | 1 | 1,877 draws, the second-busiest consumer |
| 150,000 | 3 | |
| 200,000 | 21 | the modal choice |
| 250,000 | 2 | |
| 300,000 | 4 | |
| 500,000 | 13 | 76.5% of all draws in the window |
| 650,000 | 1 | |
| 1,000,000 | 4 | |
| 1,500,000 | 1 | |
| 2,499,999 | 1 | one below the chain maximum |
| 2,500,000 | 4 | the chain maximum, from s_config() |
The verified source explains how they got there. A contract named Coinflip declares uint32 public callbackGasLimit = 1000000; and ships an onlyOwner setter beside it. The live value today is still 1,000,000, so the setter has never been called. GameManager initialises to 100,000 and has the same untouched dial. DrawCoordinator, behind the FortuneFi proxy, declares uint32 public vrfCallbackGasLimit = 2_500_000;, which hardcodes the chain’s ceiling as a default.
Four contracts have pinned themselves to 2,500,000, and that number deserves a second look, because it is not a constant. It is maxGasLimit in the coordinator’s own mutable config, and in an earlier audit of the coordinator’s admin surface I simulated the owner lowering it, including to zero, and the call succeeded. A contract sitting at the ceiling has no headroom left if its own callback grows, and the ceiling it is resting against can be moved by a four-of-nine multisig in one transaction without notice. PayloadOnChain is there already at a measured 1,061,448 gas; PackOpener has touched 2,063,193.
One contract gives a clean audit trail. CasinoCoreV2 emits VrfConfigChanged whenever its VRF settings move. Pulling that event across its entire history returns exactly one row, at block 52,337,003, setting 1,500,000. Set once, never revisited, 411 draws later.
What the chain actually used
Per draw, utilisation of the requested limit:
| Percentile | Share of limit used |
|---|---|
| Minimum | 0.52% |
| 1st | 4.74% |
| 10th | 12.55% |
| 25th | 30.52% |
| Median | 55.29% |
| 99th | 101.02% |
| Maximum | 101.36% |
The distribution is lumpy because one contract, GridMining, is 70.6% of the chain’s draws on its own, and it sits at a steady 55% of its 500,000. The thinner end is where the guessing shows. The least efficient single draw on Base this week asked for 2,500,000 and used 13,043, a factor of 192. Another contract asked 2,500,000 across seven draws and used 53,426 on three of them.
Grouped by contract rather than by draw, with each one’s worst observed callback against its own limit: GameManager 12.5%, Coinflip 7.8%, Blackjack 18.7%, PayloadOnChain 42.5%, Lotto39 57.3%, GridMining 58.7%, CasinoCoreV2 63.9%.
One cluster shows the reflex better than any single contract does. Three unverified addresses carry bytecode of identical length, 23,953 bytes, and differ only in their constructor arguments. Each made exactly 100 draws in the window. Each sends the same 500,000. Two of them need a median 62,542 gas and the third needs 11,364, which is a factor of 5.5 in actual work between deployments of the same template. The limit was copied with the code. It was never fitted to what any one of these three instances went on to do, and after a hundred rounds apiece none of them has been revisited.
The one that computed it
VRFV2PlusWrapper at 0xb0407dbe851f8318bd31404a49e658143c982f23 is Chainlink’s own direct-funding contract. It does not run a game; it passes a caller’s request through and adds its own overhead, which is why it sent nine different limits across 214 draws while every game on the chain sent one.
Its utilisation by limit, median per group, for the five groups with more than two draws (the other four limits were sent once or twice each):
| Limit requested | Draws | Median gas needed | Utilisation |
|---|---|---|---|
| 114,988 | 5 | 116,556 | 101.4% |
| 145,464 | 6 | 147,107 | 101.1% |
| 165,781 | 161 | 167,475 | 101.0% |
| 419,750 | 8 | 420,033 | 100.1% |
| 2,451,496 | 29 | 2,454,369 | 100.1% |
Those readings sit above 100% because the estimator overshoots by about one percent, as the calibration above predicted. Hand-searching the exact threshold on the largest one gives 2,451,381 gas needed against 2,451,496 requested: 115 gas spare, and it was delivered. This is what a measured limit looks like. It is not a round number, it is not generous, and it has never failed.
The tail is real, and nobody has measured theirs
The honest objection to all of the above is that a callback’s cost is not one number. It is a distribution, and the limit has to clear the worst case, not the typical one. That objection is correct, and it is precisely the thing nobody on Base has checked.
| Contract | Draws | Median gas | Worst gas | Tail |
|---|---|---|---|---|
VRFV2PlusWrapper |
214 | 167,475 | 2,454,388 | 14.66x |
FortuneFi DrawCoordinator |
7 | 70,730 | 580,605 | 8.21x |
CasinoCoreV2 |
411 | 330,745 | 961,862 | 2.91x |
PackOpener |
8 | 816,542 | 2,078,528 | 2.55x |
Blackjack |
124 | 77,420 | 186,838 | 2.41x |
GridMining |
8,987 | 276,430 | 297,034 | 1.07x |
Thirteen of the 27 contracts with five or more draws have a tail of exactly 1.00x: every callback costs the same, because the work does not depend on state. Those contracts could set a tight limit safely and have not. The ones with a real tail are the ones where the round number is doing load-bearing work by accident.
Two draws this week ran close to the edge. PackOpener needed 2,063,193 of its 2,500,000. A contract at 0xfc58d853950e427c76e7a17462da0c3ae3f0c556 needed 172,550 of 200,000, which is 86.3% of a limit that was chosen by rounding. Neither operator knows that, because neither number is published anywhere.
Does asking for more cost anything?
Almost nothing, and this matters for reading the rest. The coordinator bills actual consumption: the fee is driven by gas used, not gas offered, and on this chain both flat fees are zero with a 60% premium on native payments. The config read from s_config() confirms it: no term in the bill references the limit.
The chain agrees. Two contracts with nearly identical callbacks and wildly different limits:
| Contract | Limit | Median gas needed | Median fee paid |
|---|---|---|---|
0x204bc998... |
500,000 | 11,364 | 0.000001484 ETH |
GameManager |
100,000 | 12,548 | 0.000001440 ETH |
Five times the allowance, 3% difference in the bill. Across the week the whole chain spent 0.0457 ETH on 12,564 native-paid draws, a median of just under a cent each, and roughly 90% of a small draw’s fee is fixed overhead rather than callback gas.
To put the callback in proportion: the marginal rate works out at roughly 0.0098 gwei per unit of callback gas, so even the heaviest callback on the chain this week, PackOpener‘s 2,063,193 gas, adds about five US cents to a fee whose fixed verification and L1 data component is around a third of a cent. The callback is the small part of a small number.
What over-provisioning does cost is reserve. Summed across the 56 fixed-limit contracts at their own gas lanes, the balance they must hold is 0.674 ETH, of which 0.454 ETH is allowance beyond their own worst observed callback: about 1,089 US dollars of idle capital at today’s price, held for work that has never been required.
The lane multiplies the mistake by fifteen
Base has exactly two gas lanes, read from s_provingKeyHashes: a 2 gwei key that served 10,135 of this week’s requests, and a 30 gwei key that served 2,595. The lane is not a price, it is a ceiling on the gas price the oracle will bill you at, and it is the multiplier in the reserve formula. The same rounded limit therefore costs fifteen times as much idle capital on one lane as on the other, and nothing in a contract’s code connects the two choices.
It shows in the concentration. Three contracts account for 0.313 ETH of the chain’s 0.454 ETH of excess reserve, and all three sit on the 30 gwei lane with a limit at or one below the maximum. The worst is a single draw: a contract that asked for 2,500,000 gas, used 13,043, and on the dearer lane must keep 0.119 ETH on hand to be allowed to ask again. The contract with the most over-provisioned limit on Base is also the one paying the most to maintain it, and that pairing is a coincidence rather than a decision.
Why a generous limit is still the rational guess
Put the two sides together and the asymmetry is obvious. Set the limit too high and you lock a few dollars. Set it too low and a draw is consumed, billed and lost, with no retry path in the protocol. A rational operator who has not measured anything should round up, and that is exactly what 55 of them did.
So the finding is not that these contracts are wasteful. It is that the number guarding the delivery of their randomness was chosen by a reflex, and none of them can tell you how close they have come to it. A 200,000 limit that peaks at 172,550 is fine until a round has one more winner to pay, and the operator who has never measured the tail will not see it coming. The failure arrives as a single draw that was charged and refused.
What we do, and one new commitment
We run a prepaid subscription on the same coordinator as everyone in this census, and we are exposed to the same arithmetic. Our standing commitments stand: every round carries a deadline fixed at deployment with a permissionless refund path after it, so an entrant never needs the operator to act, and we publish the subscription’s reserve headroom rather than just its balance.
The new one, specific to tonight’s measurement: we publish our callback gas limit alongside the worst callback we have ever measured against it, and we size the limit to that measured worst case plus a stated margin rather than to a round number. The margin is a number you can argue with. A rounded 500,000 is not, because it does not encode any claim about the contract at all. Any entrant can reproduce the check with the method in the last section of this post, against us or against anyone else on Base.
What I am not claiming
- Observed worst is not true worst. Seven days of traffic is a sample of a contract’s state space, not a proof of its maximum. A raffle with more entrants than any round this week will cost more gas than anything I measured.
- The estimator is slightly generous. Every utilisation figure here is 0.1% to 2.3% higher than the hand-searched truth, which flatters the contracts, not my argument.
- Replaying at the parent block is a counterfactual. It is the same state the real callback saw, but it is still a simulation, and a contract whose gas depends on another transaction in the same block could read differently.
- A low utilisation is not a bug. Nothing here says these games are unsafe. It says their limits carry no information about them.
- Five requests in this window are still unfulfilled, and the reason is a reserve shortfall rather than a gas limit. That census is its own post.
Three questions worth asking any on-chain game
- What is your callback gas limit, and what is the most gas your callback has ever used? If the second number does not exist, the first one is a guess.
- Does your gas cost grow with the size of a round? If it does, your limit is a cap on how big a round you can settle, and you should be able to say what that cap is.
- What happens to a round whose callback runs out of gas? The protocol will not retry it for you. Either your contract has a path, or the entrants do not get paid.
How to redo this
Pull eth_getLogs on the coordinator in 1,000-block spans from an archive endpoint and decode RandomWordsRequested, where key hash, subscription and sender are indexed and the request id, pre-seed, confirmations, callback gas limit and word count are in the data. Match each to RandomWordsFulfilled on request id to recover outputSeed. Rebuild the words as keccak256(abi.encode(outputSeed, i)), pack them into rawFulfillRandomWords, and run eth_estimateGas from the coordinator address at the block before the fulfilment. Subtract 21,000 plus the calldata cost. Run the same call from an unrelated address as your control, and hand-search the exact threshold with explicit gas caps on a handful of draws to calibrate the estimator. Contract configuration comes from s_config() on the coordinator and from the verified source of each consumer; config-change events, where a contract emits them, show whether the dial has ever been turned.
Every figure above came from public logs, public state and simulated calls. No permission, no API key, no operator’s cooperation. Which is the point: on a chain where the limit is the thing that decides whether your randomness arrives, nobody should have to take an operator’s word for where it sits.
Data: Base mainnet blocks 52,520,200 to 52,822,599, seven days to 19 October 2026. Prices from CoinGecko the same evening, ETH at 2,398.49 US dollars.
📷 Photo by Wesley Tingey on Unsplash


