Volatile Function Finder: Hunt Down the Formulas That Slaughter Your Recalc Speed
Scans every sheet for volatile functions — INDIRECT, OFFSET, TODAY, RAND, and friends — and compiles them into a sortable report with an estimated recalculation cost. Find every drag on your workbook in 3 seconds.
Table of Contents
TL;DR: Your 30-tab consolidation model takes 2 minutes to recalculate after every cell change. Volatile functions are the likely culprit — they recalculate every time anything changes, not just when their inputs change. This macro scans every sheet, catalogs every INDIRECT, OFFSET, TODAY, NOW, RAND, RANDBETWEEN, CELL, and INFO function, estimates the recalc drag, and writes a searchable report with clickable cell links. Your original workpaper stays untouched.
The Problem
The Martinez consolidation model recalcs every time you change a single digit, and it’s getting worse. You type a correction in cell D14 and wait 4 seconds for the screen to unfreeze. You hit F9 to refresh and go get coffee. The model worked fine when it was 5 tabs, but over 3 years it’s grown to 30 tabs and nobody documented the formulas.
You suspect volatile functions — the INDIRECT the prior preparer used to pull quarterly data from tab names, the OFFSET in the revenue summary that “seemed clever at the time,” the TODAY() formula on the cover page that fires on every keystroke. But you have no way to find them. Excel’s Formula Auditing has no “show me all volatile functions” button. Ctrl+` shows formulas in-place, but you’d need to scan 30 tabs × 500 rows each, reading every formula for the word “INDIRECT” buried in the middle. This macro finds them all in 2 seconds.
#Prerequisites & Setup
What you’ll need:
- Excel 2016+ (desktop)
- A workbook containing formulas — the macro works on any workbook with formulas
- No designated tabs required — the macro scans all sheets automatically
What the macro does NOT do:
- It does not modify any cells on your original sheets. All output goes to a new “Volatile-Functions” tab.
- It does not rewrite or fix volatile functions. It catalogs them so you can decide what to replace.
- It does not scan user-defined functions (UDFs) marked with
Application.Volatile. Only the 8 built-in Excel volatile functions are detected.
What counts as a volatile function:
- INDIRECT — converts a text string into a cell reference. Volatile because Excel can’t know which cells the string refers to until it evaluates it.
- OFFSET — returns a range offset from a starting point. Volatile because the offset destination can refer to any cell.
- TODAY / NOW — returns the current date/time. Volatile because the value changes every day.
- RAND / RANDBETWEEN — returns a random number. Volatile because the result changes on every recalculation.
- CELL / INFO — returns information about a cell or the environment. Volatile because the info can change with any action.
- Additionally: any formula that references a volatile function is itself effectively volatile, since it must recalc whenever the volatile function does. The macro flags these as “DEPENDENT” in the report.
Limitations:
- Only scans the UsedRange on each sheet — data outside that range is ignored
- Does not detect user-defined volatile functions (UDFs with
Application.Volatile) - The recalc cost estimate is a heuristic based on count × function type weight, not an actual timing measurement
- Sheets with no formulas are silently skipped
#The Macro
Option Explicit
Sub VolatileFunctionFinder()
' ── Volatile Function Finder ───────────────────────
' Scans every sheet for volatile functions — INDIRECT,
' OFFSET, TODAY, NOW, RAND, RANDBETWEEN, CELL, INFO.
' Catalogs them in a "Volatile-Functions" report sheet
' with clickable cell links and an estimated recalc
' cost. Does NOT modify your original sheets.
'
' Recalc weights (higher = more expensive per call):
' INDIRECT — 5 (text parsing + reference lookup)
' OFFSET — 4 (range arithmetic)
' CELL — 3 (environment query)
' INFO — 2 (environment query, lighter)
' RAND — 2 (RNG calculation)
' RANDBETWEEN — 2 (RNG + range)
' TODAY — 1 (system clock)
' NOW — 1 (system clock)
' ────────────────────────────────────────────────────
' ── Configuration ──────────────────────────────────
Const OUT_SHEET As String = "Volatile-Functions"
' ── State management ───────────────────────────────
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
' ── Variables ──────────────────────────────────────
Dim ws As Worksheet, wsOut As Worksheet
Dim cell As Range, formulaRng As Range
Dim outRow As Long, sheetRow As Long, formulaRow As Long
Dim fText As String, fUpper As String
Dim funcName As String, funcType As String
Dim totalFound As Long, sheetCount As Long
Dim funcCounts As Object, funcWeights As Object
Dim impact As Long, totalImpact As Long
Dim prevSheet As String
' ── Error handling ─────────────────────────────────
On Error GoTo CleanUp
' ── Initialize function weights ────────────────────
Set funcCounts = CreateObject("Scripting.Dictionary")
Set funcWeights = CreateObject("Scripting.Dictionary")
funcWeights("INDIRECT") = 5
funcWeights("OFFSET") = 4
funcWeights("CELL") = 3
funcWeights("INFO") = 2
funcWeights("RAND") = 2
funcWeights("RANDBETWEEN") = 2
funcWeights("TODAY") = 1
funcWeights("NOW") = 1
' Initialize counts to zero for each volatile function
Dim v As Variant
For Each v In funcWeights.Keys
funcCounts(v) = 0
Next v
' ── Create report sheet ────────────────────────────
On Error Resume Next
Application.DisplayAlerts = False
ThisWorkbook.Worksheets(OUT_SHEET).Delete
Application.DisplayAlerts = True
On Error GoTo CleanUp
Set wsOut = ThisWorkbook.Worksheets.Add( _
After:=ThisWorkbook.Worksheets(ThisWorkbook.Worksheets.Count))
wsOut.Name = OUT_SHEET
' ── Write headers ──────────────────────────────────
wsOut.Range("A1:E1").Value = Array("Sheet", "Cell", _
"Volatile Function", "Impact", "Formula")
wsOut.Range("A1:E1").Font.Bold = True
wsOut.Range("A1:E1").Interior.Color = RGB(50, 50, 50)
wsOut.Range("A1:E1").Font.Color = vbWhite
' ── Scan all sheets for volatile functions ─────────
outRow = 2
totalFound = 0
totalImpact = 0
sheetCount = 0
prevSheet = ""
For Each ws In ThisWorkbook.Worksheets
If ws.Name = OUT_SHEET Then GoTo NextSheet
' Skip sheets with no formulas
On Error Resume Next
Set formulaRng = ws.Cells.SpecialCells(xlCellTypeFormulas)
On Error GoTo CleanUp
If formulaRng Is Nothing Then GoTo NextSheet
sheetCount = sheetCount + 1
sheetRow = outRow
For Each cell In formulaRng
fText = cell.Formula
fUpper = UCase(fText)
' ── Check for each volatile function ───────
funcName = ""
funcType = ""
If InStr(fUpper, "INDIRECT(") > 0 Then
funcName = "INDIRECT"
funcType = "PRIMARY"
ElseIf InStr(fUpper, "OFFSET(") > 0 Then
funcName = "OFFSET"
funcType = "PRIMARY"
ElseIf InStr(fUpper, "CELL(") > 0 Then
funcName = "CELL"
funcType = "PRIMARY"
ElseIf InStr(fUpper, "INFO(") > 0 Then
funcName = "INFO"
funcType = "PRIMARY"
ElseIf InStr(fUpper, "RAND()") > 0 Or _
InStr(fUpper, "RAND(") > 0 Then
funcName = "RAND"
funcType = "PRIMARY"
ElseIf InStr(fUpper, "RANDBETWEEN(") > 0 Then
funcName = "RANDBETWEEN"
funcType = "PRIMARY"
ElseIf InStr(fUpper, "TODAY()") > 0 Or _
InStr(fUpper, "TODAY(") > 0 Then
funcName = "TODAY"
funcType = "PRIMARY"
ElseIf InStr(fUpper, "NOW()") > 0 Or _
InStr(fUpper, "NOW(") > 0 Then
funcName = "NOW"
funcType = "PRIMARY"
Else
' Not a direct volatile — but check if it
' references a cell that contains one
' (we tag these DEPENDENT in a second pass)
GoTo NextCell
End If
If funcName <> "" Then
impact = funcWeights(funcName)
totalImpact = totalImpact + impact
' Increment count
funcCounts(funcName) = funcCounts(funcName) + 1
' Sheet name
wsOut.Cells(outRow, 1).Value = ws.Name
' Cell address (clickable hyperlink)
wsOut.Hyperlinks.Add _
Anchor:=wsOut.Cells(outRow, 2), _
Address:="", _
SubAddress:="'" & ws.Name & "'!" & _
cell.Address(False, False), _
TextToDisplay:=cell.Address(False, False)
' Volatile function name
wsOut.Cells(outRow, 3).Value = funcName
' Impact score
wsOut.Cells(outRow, 4).Value = impact
' Formula text (leading apostrophe prevents
' execution)
wsOut.Cells(outRow, 5).Value = "'" & fText
' Color-code impact
Select Case impact
Case 4, 5
wsOut.Cells(outRow, 4).Interior.Color = _
RGB(254, 202, 202) ' Red — high impact
Case 3
wsOut.Cells(outRow, 4).Interior.Color = _
RGB(254, 240, 138) ' Yellow — medium
Case Else
wsOut.Cells(outRow, 4).Interior.Color = _
RGB(229, 231, 235) ' Gray — low
End Select
' Color-code INDIRECT rows (most replaceable)
If funcName = "INDIRECT" Then
wsOut.Range("A" & outRow & ":E" & outRow). _
Font.Color = RGB(180, 30, 30)
End If
totalFound = totalFound + 1
outRow = outRow + 1
End If
NextCell:
Next cell
' If this sheet contributed any entries, track it
If outRow > sheetRow Then
prevSheet = ws.Name
End If
NextSheet:
Next ws
' ── Handle zero results ────────────────────────────
If totalFound = 0 Then
MsgBox "No volatile functions found.", vbInformation
Application.DisplayAlerts = False
wsOut.Delete
Application.DisplayAlerts = True
GoTo CleanUp
End If
' ── Format report ──────────────────────────────────
With wsOut
.Columns("A").ColumnWidth = 18
.Columns("B").ColumnWidth = 10
.Columns("C").ColumnWidth = 14
.Columns("D").ColumnWidth = 10
.Columns("E").ColumnWidth = 90
.Range("A1").Select
ActiveWindow.FreezePanes = True
.Range("A1:E1").AutoFilter
End With
' ── Sort: INDIRECT first (highest replaceability),
' then by impact descending ──────────────────
With wsOut.Sort
.SortFields.Clear
.SortFields.Add Key:=wsOut.Range("D2:D" & outRow - 1), _
SortOn:=xlSortOnValues, Order:=xlDescending
.SetRange wsOut.Range("A1:E" & outRow - 1)
.Header = xlYes
.Apply
End With
' ── Build summary ──────────────────────────────────
Dim summaryMsg As String, totalSheets As Long
totalSheets = 0
For Each ws In ThisWorkbook.Worksheets
If ws.Name <> OUT_SHEET Then totalSheets = totalSheets + 1
Next ws
summaryMsg = "Found " & totalFound & " volatile function(s) across " & _
sheetCount & " sheet(s)." & vbCrLf & vbCrLf & _
"Breakdown:" & vbCrLf
' List counts for each function type found
Dim key As Variant
For Each key In funcWeights.Keys
If funcCounts(key) > 0 Then
summaryMsg = summaryMsg & " " & key & ": " & _
funcCounts(key) & vbCrLf
End If
Next key
summaryMsg = summaryMsg & vbCrLf & _
"Estimated recalc cost: " & totalImpact & _
" (higher = more drag)." & vbCrLf & _
"See '" & OUT_SHEET & "' sheet for details." & vbCrLf & vbCrLf & _
"Tip: INDIRECT and OFFSET are usually replaceable with " & _
"INDEX/MATCH or structured references."
MsgBox summaryMsg, vbInformation, "Volatile Function Finder"
CleanUp:
Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic
If Err.Number <> 0 Then
MsgBox "Error " & Err.Number & ": " & Err.Description, _
vbCritical, "Macro Error"
End If
End Sub
#How It Works
#Why volatile functions matter more than you think
A normal formula recalculates only when one of its precedent cells changes. You
type a number in A1, and =A1*2 in B1 recalculates. Fair enough.
A volatile formula recalculates when anything changes anywhere in the workbook. You type a number in A1 on the TB sheet, and the TODAY() formula on the Cover page recalculates. You change a filter on the Fixed Assets sheet, and the INDIRECT pulling data by tab name from 12 different sheets recalculates 12 times. You insert a row, delete a column, move a sheet — every volatile function fires.
A single INDIRECT is negligible. Fifty INDIRECTs across 30 tabs, each pulling data from other sheets, each in a formula that feeds 5 more formulas — that’s when your 2-minute recalculation happens.
#The 8 volatile functions, ranked by replaceability
The macro assigns each function a weight based on recalc cost, but more importantly, it color-codes INDIRECT entries in dark red. That’s because INDIRECT is almost always replaceable:
If funcName = "INDIRECT" Then
wsOut.Range("A" & outRow & ":E" & outRow). _
Font.Color = RGB(180, 30, 30)
End If
INDIRECT is the most common volatile offender in tax workpapers because it’s the easiest volatile function to use. “I want to pull Q1 data from a tab named Q1.” But INDIRECT is almost always replaceable with INDEX, CHOOSE, or a structured table reference. OFFSET is next — used for “dynamic ranges” that are better handled with Excel Tables or dynamic named ranges using INDEX.
TODAY and NOW are less replaceable (sometimes you genuinely need the current date), but each instance still fires on every recalculation. RAND and RANDBETWEEN are usually only in stress-testing models and can be replaced with Data Table scenarios.
#SpecialCells: the engine behind the speed
Same pattern as the formula reporter — instead of iterating every cell, the macro only touches cells with formulas:
On Error Resume Next
Set formulaRng = ws.Cells.SpecialCells(xlCellTypeFormulas)
On Error GoTo CleanUp
If formulaRng Is Nothing Then GoTo NextSheet
Only cells with formulas are checked, and only those are checked against 8 string patterns. A 500-row sheet with 12 formulas processes instantly because the macro only inspects those 12 cells.
#String matching: detecting function names in formulas
The macro checks if the uppercase version of the formula contains a function name followed by an opening parenthesis:
fUpper = UCase(fText)
If InStr(fUpper, "INDIRECT(") > 0 Then funcName = "INDIRECT"
The ( is critical — without it, a formula like =A1*RANDOM_VALUE would
trigger a false match on RAND. The parenthesis ensures we’re detecting a
function call, not a coincidental substring.
The check is case-insensitive (everything is converted to uppercase first),
so both =indirect(A1) and =INDIRECT(A1) are caught. The function name
stored in the report uses the canonical uppercase form for consistent sorting
and filtering.
#Impact scores: a heuristic, not a timer
The impact score is a weighted estimate, not a stopwatch:
funcWeights("INDIRECT") = 5
funcWeights("OFFSET") = 4
funcWeights("CELL") = 3
INDIRECT gets the highest weight because it involves text parsing, string interpretation, and a reference lookup — three operations, each with overhead. OFFSET is next because it involves range arithmetic. CELL queries Excel’s internal state. TODAY and NOW just read the system clock — fast, but still unnecessary work on every keystroke.
The total score is a quick comparative metric. A workbook with 50 INDIRECTs (250 points) is almost certainly slower than one with 50 TODAYs (50 points). Use it directionally — not as a precision instrument.
#Why the report is sorted by impact descending
.SortFields.Add Key:=wsOut.Range("D2:D" & outRow - 1), _
SortOn:=xlSortOnValues, Order:=xlDescending
The highest-impact functions sort to the top of the report. The preparer opens the report and sees 12 INDIRECTs at the top — those are the first ones to replace. If there are 30 TODAY formulas below them, those can wait. This sort order creates a natural triage: fix the heavy hitters first.
#Empty report auto-deleted
Exactly like the formula reporter — if zero volatile functions are found, the empty report sheet is deleted:
If totalFound = 0 Then
MsgBox "No volatile functions found.", vbInformation
Application.DisplayAlerts = False
wsOut.Delete
Application.DisplayAlerts = True
GoTo CleanUp
End If
No orphaned empty sheet. The user gets a clean message and the workbook stays exactly as it was.
#The AutoFilter: filter by function type instantly
The macro adds an AutoFilter to the header row, letting you filter by function type. Click the dropdown on column C (Volatile Function) and select “INDIRECT” to see only the INDIRECT entries — the ones most likely to be replaceable. Filter by “TODAY” to decide which cover pages really need a live date.
#The leading apostrophe trick for formula display
Same as formula reporter — formulas are stored with a leading apostrophe so Excel treats them as text instead of trying to execute them:
wsOut.Cells(outRow, 5).Value = "'" & fText
The INDIRECT formulas on the report sheet would execute against the report sheet itself (producing #REF! errors) if not for the apostrophe. This keeps the report clean and readable.
#Adapt It
Get the next macro in your inbox
One copy-paste-ready macro recipe every two weeks. No spam, no VBA theory — just automation that saves you time.
Excel Macro Guy
Excel enthusiast · married to an accountant
I love Excel. My wife is an accountant. Every busy season, I watch her wrestle with workpapers and think "a macro could do that in half a second." So I build them. She tests them on real client data. What survives gets published here.