The Dependency Graph
Terraform turns your configuration into a directed acyclic graph — every resource and data source is a node, every reference is an edge — and the entire apply is just a walk of that graph in dependency order. The graph is not a feature you opt into; it is the model underneath everything the previous two topics described, and seeing it explicitly makes the workflow's behavior stop being mysterious.
Understanding the DAG explains why Terraform creates hatch-events-raw before the Pub/Sub notification that points at it, why three independent resources build at the same time, and why a reference cycle is a hard error rather than something Terraform quietly guesses its way through. The order you never wrote is the graph doing its job.
The DAG
The nodes of the graph are resources, data sources, variables, and outputs. The edges are references and explicit depends_on entries. The "acyclic" part is enforced, not aspirational: a cycle — A depends on B which depends on A — has no valid execution order, so rather than pick one arbitrarily, Terraform refuses to run and reports the cycle. A graph with no cycles always has at least one valid order, and that guarantee is what makes the apply deterministic.
How Walk Order Falls Out of References
You never write the order; it is derived. The hatch-events-raw bucket node has no inbound edge, so it has nothing to wait on and builds first. The notification references the bucket, so an edge points from notification to bucket, and the notification builds after. The event-processor service references the events-ingest topic, so it builds after that. Walk the edges and the creation order is fully determined — file order is irrelevant.
Parallelism
Terraform walks independent branches of the graph concurrently, defaulting to 10 simultaneous operations. In Layer A, the event-processor service account, the analytics dataset, and an enrichment-api-key secret have no edges between them — none references another — so all three are created at the same time rather than one after the next. The graph's width is the apply's speed.
This is why an accidental edge is costly. Two resources that did not need ordering, joined by a stray reference or a broad depends_on, can no longer run in parallel — one now waits on the other for no reason. Keeping the graph wide keeps the apply fast.
Tuning with -parallelism
-parallelism=N raises or lowers how many operations run at once. The reason to lower it is a quota: when a bulk create of many resources trips a strict GCP API quota — Compute Engine or Service Usage limits measured per minute — -parallelism=2 throttles Terraform to stay under the ceiling instead of retrying blindly into the same wall. You can raise it cautiously to speed a large apply, but treat it as a tuning knob, not a fix.
Crucially, -parallelism never changes the order — it only caps concurrency within the order the graph already dictates. It is not a tool for solving a dependency problem; a missing edge is fixed with a reference, not by slowing everything down until the race stops happening.
Inspecting the Graph
terraform graph emits the DAG in Graphviz DOT format, which you render to an image with dot. It is how you see why an edge exists, find an accidental dependency that is serializing your apply, or make sense of a confusing creation order. When an apply is slower than the resource count suggests it should be, the rendered graph usually shows a single stray edge collapsing two parallel branches into one.
# emit DOT and render with Graphviz
terraform graph | dot -Tsvg > graph.svg
When Terraform prints Cycle: followed by a list of addresses, it has found a genuine loop in your config — resource A references B and B references A, often indirectly through an output or a depends_on chain. It is not a Terraform bug.
The fix is always to break the loop, not to retry. The usual move is to extract the shared value into a local or a variable that both resources read, so neither has to reference the other. Adding more depends_on typically deepens the cycle rather than resolving it.
- Creating a reference cycle — resource A references B and B references A, often via outputs or a
depends_onloop — Terraform errors with "Cycle:" and refuses to run until you break it. - Raising
-parallelismto speed up a largehatch-pipeline-devapply and hitting Compute Engine or Service Usage API quota errors — more concurrency means more simultaneous API calls against a fixed per-minute quota. - Assuming resources apply top-to-bottom in the file and writing config that "looks ordered" — file order is irrelevant; only the edges decide, and a missing reference means no ordering guarantee at all.
- Adding a stray
depends_onthat creates an edge between two otherwise-independent resources and quietly serializing them — the apply slows andterraform graphis the only way to see why. - Treating a "Cycle:" error as a Terraform bug rather than a real circular reference in your own config — the fix is always to break the loop, often by moving a value to a variable or splitting a resource.
- Lowering
-parallelismpermanently to dodge a quota error instead of requesting a quota increase — you slow every future apply to work around a limit you could have raised.
- Let references define order and run
terraform graph | dot -Tsvgwhen an apply's ordering or slowness is surprising. - Lower
-parallelismwhen a bulk create trips a GCP API quota, rather than retrying blindly into the same limit. - Break dependency cycles by introducing an intermediate variable or local instead of adding
depends_on, which can deepen the cycle. - Keep
depends_onedges minimal so the graph stays wide and the apply stays parallel. - Inspect
terraform graphbefore assuming a slow apply is unavoidable — a single accidental edge is often the whole cause.
Knowledge Check
Why must the dependency graph be acyclic?
- A cycle has no valid execution order, so Terraform refuses to run rather than pick one arbitrarily
- Cycles make the state file grow without bound on every subsequent apply
- A cycle would force every resource caught inside the loop to be created twice over, doubling all the work
- Graphviz cannot render the DOT output for a graph that contains a cycle
Three resources with no edges between them are in your config. How does Terraform create them?
- Concurrently, since independent branches of the graph are walked in parallel up to the parallelism limit
- Sequentially, one by one, in the exact top-to-bottom order in which they happen to appear in the configuration file
- One at a time, sorted alphabetically by their full resource address
- In a random order that is chosen freshly on every single apply
A bulk create is hitting a per-minute GCP API quota. What is the right use of -parallelism?
- Lower it so fewer API calls fire at once and the apply stays under the quota
- Raise it so all the calls finish before the per-minute quota window resets
- Set it to 1 permanently, since that is the only safe value when running on GCP
- Use it to reorder the graph so the quota-limited resources go last
An apply is far slower than its resource count suggests. What is the most useful diagnostic?
- Render
terraform graphto spot a stray edge that is serializing two otherwise-independent branches - Keep raising
-parallelismincrementally until the apply finally speeds up - Reorder the resource blocks in the file to match the desired creation order
- Add a
depends_onedge between the two slow resources to force them to start up and run together in parallel
You got correct