The dependency graph behind every Terraform plan and apply
A production Terraform or OpenTofu estate is a graph of thousands of nodes spread across dozens of states, and treating that graph as simple is why plans crawl, and blast radius becomes hard to estimate.
terraform graph starts to fall apart at scale.Every engineer already reasons in dependency graphs, whether or not they'd call it that. Decide that the application server can't come up before the database exists, and you've just traced an edge between two nodes.
Terraform and OpenTofu do the same thing on every run: before either tool touches a resource, it builds a dependency graph of your entire configuration. That graph's shape determines what gets evaluated in what order, what runs in parallel, and what fails with a cycle error before anything real gets touched.
This article quickly covers the general, computer-science definition of a dependency graph (you probably already know what a directed graph is), then goes deep on how Terraform and OpenTofu build and walk their own graph, where that per-state model runs into real limits at production scale, and how to actually generate and read a visualization of your own graph – whether or not you use Stategraph.
You'll be better able to work out how different parts of your state relate to each other, whether you're chasing an Error: Cycle: message, explaining a slow plan, or scoping a change's blast radius before you run it.
What is a dependency graph?
In computer science, a dependency graph is a directed graph representing dependencies between several objects, where components depend on the ones they need to resolve first.
One node points to another when the first depends on the second, and that structure lets a system process a pile of unordered components in a structured way and determine an optimal order to run them in.
Build systems, package managers, spreadsheet engines, and compilers across countless software systems all lean on the same underlying model, whether the domain is source code, infrastructure, or a spreadsheet's own cell formulas.
Nodes, edges, directions, and direct dependencies
A node is whatever unit the system tracks: a resource, a module, a package, a file, a cell. An edge is a directed connection between two nodes, read as "depends on," and the direction is what makes the graph directed rather than a plain network diagram; it encodes which way evaluation has to flow.
A direct dependency is the simplest version of that relationship: one node explicitly referencing another with nothing sitting between them, whether that's one file importing another, one library listing a second as required in a manifest, or one resource reading another resource's attribute.
Most tools can automatically generate direct dependencies just by parsing the reference itself, without any manual mapping.
Transitive dependencies and dependency chains
A transitive dependency is inherited through an intermediate node rather than declared directly: if A depends on B, and B depends on C, then A has a transitive dependency on C even though nothing in A's own definition names C.
Follow these dependency chains carefully, not just the direct edges nearest any single node, to work out what tends to break: a change to C looks safe if you only check what references it directly, and turns out unsafe once you trace the chain back through B to A and everything else hanging off it.
The deeper the chain, the more important this is, and production systems build genuinely deep ones. A graph with many layers between its root nodes and its leaves is often the default, rather than an edge case, in a system that's been running for more than a few months.
How Terraform and OpenTofu build and walk a dependency graph
Both Terraform and OpenTofu construct a literal graph from your .tf files and current state on every run.
That graph is the mechanism behind plan and apply. Skip the graph, and there's no plan at all.
Building the graph from configuration and state
Before evaluating anything, Terraform parses every resource, data source, and module block in your configuration into a node. Every reference between them – a resource attribute pointing at another resource's output, a module input reading a variable, or a data source consumed inside a for_each, for example – becomes a directed edge.
This construction runs whether you call plan, apply, destroy, or validate; it's a required pass over your configuration and state, not an optional analysis you can switch off.
Determining evaluation order for plan and apply
Once the graph exists, Terraform performs what's effectively a topological walk of it: starting at nodes with no unresolved dependencies and working outward, so nothing evaluates before everything it points to already has.
That same walk is how Terraform identifies which resources are independent of each other, since two nodes with no path connecting them, directly or transitively, can be evaluated concurrently with no risk of one reading a value the other hasn't produced yet.
Transitive dependencies between resources and modules
The clearest version of a transitive dependency in Terraform is a resource that reads a module's output, where that output itself resolves from a resource buried inside the module.
Say an aws_instance reads module.network.subnet_id, and subnet_id is itself an output pointing at aws_subnet.app.id. The instance has no direct reference to the subnet resource anywhere in its own configuration, but it can't be created before that subnet exists, so Terraform has to resolve the full chain rather than just the immediate module reference.
Chains that pass through more than one module work the same way. The whole graph has to resolve correctly, not just the edges each module declares on its own.
Cycle detection and the "Error: Cycle" message
Circular dependencies occur when following edges long enough leads back to the node you started from, most often when two resources each reference an attribute of the other, so neither can be created first.
Terraform detects this during graph construction and refuses to guess: rather than picking a starting point and hoping the rest resolves, it fails with an Error: Cycle: message naming the nodes involved.
An arbitrarily chosen evaluation order against a cyclic graph isn't a valid order at all, just a guess that happens to run until it doesn't.
Where Terraform's graph model runs into limits at scale
None of this is a flaw in how Terraform was designed.
Terraform has a graph, and scoping it to a single state was a reasonable choice for a single workspace; it works exactly as intended at that scale. The constraint shows up once an organization runs many states instead of one.
One graph per state, not one graph per system
Terraform builds one graph per state, and that graph only knows about the resources and modules that state manages.
A terraform_remote_state data source, or a hardcoded reference to a resource ID living in another state, creates a real dependency between two systems. The consumer's graph sees only a data source node, and the producer's graph sees nothing at all, so neither side captures the relationship between the actual resources.
Run terraform graph against any one state in an organization running dozens of them, and what you get back is accurate for that state and blind to everything crossing its boundary; no single graph shows the true blast radius of a change that reaches across states, because no single graph contains it.
Why bigger graphs mean slower plans and applies
Node count alone slows things down, independent of how complex any individual resource is.
Walking large graphs and re-evaluating dependency chains on each run scales with the size of the whole graph, not just the resources actually changing, so a state that's grown to thousands of resources across dozens of modules pays that cost on every plan – even one that only touches a single node.
If you split a state, it helps the walk itself but reintroduces the cross-state blind spot above, which makes this a genuine constraint to design around rather than a problem with a free fix.
How Stategraph makes the dependency graph an asset instead of a bottleneck
This is why we built Stategraph.
Stategraph operates on top of the graph model Terraform and OpenTofu already compute, changing what happens to that graph.
Storing the dependency graph instead of recomputing it every run
We persist the infrastructure dependency graph in a database instead of rebuilding it from scratch on every invocation, which makes everything below it possible.
Once the graph exists as queryable data rather than an ephemeral structure that disappears the moment a plan finishes, it can be reasoned about between runs, and it doesn't have to stay scoped to a single state.
We can represent connections between resources that live in entirely different states, closing the cross-state blind spot named above, because we store dependency relationships as data rather than recomputing them from one state's configuration each time.
Operating on the affected subgraph, not the whole state
Stategraph doesn't need to walk an entire state to work out what a change touches, because the graph is already stored.
It can identify the subgraph a change actually affects (the node itself, plus everything reachable from it along dependency edges) and operate on just that subgraph instead of re-evaluating everything alongside it.
It's a direct fix for both limits above: a change scoped to one corner of a large graph doesn't get slower just because the rest of the graph grew, and a subgraph spanning multiple states is still a single subgraph as far as execution is concerned, since the graph it's drawn from was never scoped to one state to begin with.
Blast radius analysis, parallel execution, and resource-level locking
Blast radius analysis is impact analysis made concrete: the downstream subgraph from one changed node, visible before you apply anything, so "what will this affect" becomes a query against the stored graph instead of a guess.
Parallel execution occurs when Stategraph identifies non-overlapping subgraphs, the same independence Terraform's own topological walk relies on, just applied across a wider graph, and evaluates them concurrently.
Resource-level locking carries that same graph awareness into concurrency safety: instead of one lock covering an entire state file and blocking every other change regardless of overlap, locking happens at the level of the individual resources a change actually touches, so two non-overlapping changes to the same state can run at the same time instead of queuing behind each other.
Visualizing a dependency graph
Everything above is worth verifying against your own configuration, and a dependency graph visualization is the fastest way to do it. Here's a concrete, reproducible set of instructions for going from a real Terraform configuration to an actual picture of its graph.
Generating a graph with terraform graph
Terraform ships a built-in command for exactly this. terraform graph, run from an initialized configuration, emits the graph Terraform itself computed, in DOT format, the plain-text graph description language Graphviz and most other tools read natively.
Each line declares a node or an edge; something like "aws_instance.web" -> "aws_subnet.app" reads as "the instance depends on the subnet," matching the direction Terraform resolves during a real plan.
Try it first against a small configuration with four or five resources to get a feel for how verbose the raw output is even for a toy example; a production state's DOT output can run into thousands of lines, which is what shapes the limit covered below.
Rendering the DOT output into an actual diagram
DOT text on its own isn't a diagram; it needs a renderer. The most direct path pipes it straight into Graphviz's dot command:
That produces an SVG you can open in any browser. If Graphviz isn't installed locally, pasting the same DOT text into an online Graphviz viewer works identically, since the renderer only needs the text, not a live Terraform run.
Here's what that output looks like for a small, realistic configuration – a VPC, a subnet, a security group, and an instance – simplified from Terraform's actual node naming for readability:
Render that through dot -Tsvg and you get four nodes and four directed edges: exactly the graph Terraform would build for a configuration declaring those same four resources, with the instance depending transitively on the VPC through the subnet.
Why the rendered image is no longer useful at enterprise scale
A rendered SVG of a four-node graph is genuinely useful. The same render of a state with thousands of resources across dozens of modules becomes what practitioners call a hairball: a dense tangle of overlapping lines and unreadable labels, long before the state reaches full production scale.
A static image also can't be queried or filtered, and it goes stale the moment the configuration changes again. Visualizing a graph is necessary for understanding its shape, but a picture alone was never going to be enough for operating on one at real scale.
Visualizing and querying the graph in Stategraph
Stategraph keeps that limitation from recurring by rendering the visualization against the same graph it stores, not a one-off export: because the graph lives in a database, it stays current as configuration changes, and you can query the underlying model directly, using SQL, instead of tracing visually line by line.
Dependency graph tools: general-purpose vs. IaC-aware
General-purpose dependency graph tools – Graphviz, Mermaid, and code-analysis tools built for legacy codebases to spot dead code or unused imports – are actually useful for a quick dependency analysis, and cost nothing beyond the time to install them.
They'll render any DOT or graph description handed to them, whether it came from terraform graph, a package manager's dependency tree, or a hand-drawn diagram of a legacy call graph – some even flag incompatible licenses across a project's third-party libraries.
What they don't have is any notion of what a Terraform or OpenTofu graph represents: they can't distinguish a provider dependency from a resource dependency, and they don't know a graph is scoped to one state.
Module boundaries don't register either, beyond whatever labels happen to appear in the DOT text.
IaC-aware tooling, Stategraph included, starts from the same graph but keeps that domain knowledge attached, so state, provider ordering, and module structure stay part of the model instead of flattening into generic nodes and edges.
Conclusion
Terraform and OpenTofu build a literal dependency graph on every plan and apply, walk it to determine evaluation order and catch cycles before they reach real infrastructure, and scope it to a single state – a boundary that holds up fine until an organization runs many states with dependencies crossing between them.
Visualizing your own graph with nothing more than terraform graph and Graphviz is worth doing regardless of scale; it's the shortest path to see your configuration's actual structure instead of the mental model you're assuming.
Once that graph is too large to read as a picture, or your dependencies have already crossed state boundaries, try Stategraph free and see what your own infrastructure's dependency graph looks like at the scale you're actually running it.
Dependency graph FAQs
How do I enable dependency graphs in GitHub?
The GitHub dependency graph is on by default for public repositories. For a private repository, a repository admin can turn it on from the repository's Settings tab: under Security, open Advanced Security and click Enable next to Dependency graph.
GitHub's dependency graph tracks software packages declared in manifest files and lock files – the package-ecosystem graph behind Dependabot alerts and software bill of materials (SBOM) exports – a different graph from the Terraform or OpenTofu resource dependency graph covered above, which sequences infrastructure resources rather than third-party libraries and vulnerable packages.
Is a dependency graph the same thing as a directed acyclic graph (DAG)?
Not quite. A dependency graph is a directed graph, full stop, and it only becomes a directed acyclic graph (DAG) once it has no cycles at all.
Terraform's cycle detection is necessary because a dependency graph with a circular dependency in it isn't a DAG, and a graph that isn't a DAG has no valid topological order.
Can I generate a dependency graph without running terraform plan?
Yes. terraform graph only needs an initialized configuration; it reads your configuration and whatever state already exists, then emits the graph without evaluating providers or computing an actual plan.
It's different from terraform plan, which walks the same graph but also refreshes resource data and computes real diffs, which is why you can generate the graph against a configuration you haven't applied yet.