When terraform plan Is Slow: Refresh Load, Parallelism, and the Stack-Split Signal
As a stack grows, there comes a day when terraform plan starts taking minutes. The conclusion up front: the bulk of plan time is refresh — the step that reads the real API for every resource in state to confirm its current condition — so duration is set by resource count times API round trips, not by code complexity. The remedies therefore aim at reducing API reads rather than optimizing computation, and when the options run out, splitting the stack is the structural fix. How state and plan work underneath is covered in Basics #5, what state really is.
Where the time goes: the anatomy of refresh #
Plan moves in three steps: read state, query the real condition of every resource recorded in state through the provider (refresh), then compare against the code to compute changes. The final computation is local and handles thousands of resources instantly. The bottleneck is the middle: at least one API call per resource.
A 500-resource stack means 500-plus API round trips. Even divided by the default parallelism of 10, latency stacks up — and the bigger problem is AWS API rate limits. When calls bunch up, throttling kicks in, the provider backs off and retries, and perceived time degrades sharply. Check CloudTrail on a slow stack and you will find a wall of Describe calls; that is this structure.
The option ladder: correct uses and limits #
-refresh=false skips refresh and trusts state as-is. The effect is dramatic, but it rests on a premise: state matches reality. The legitimate use is the middle of a development loop where you are editing code and re-running plan repeatedly. You know everything was just refreshed, so skipping on the repeat runs is safe. The final plan before apply and any plan posted for PR review must include refresh: skip it with drift present and you are computing changes against a reality that does not exist. Drift management itself is the subject of Ops #2.
-parallelism adjusts the concurrent operation count from its default of 10. On large stacks that are not yet throttling, 20–30 can shorten the wall clock; on stacks already hitting rate limits, raising it increases throttling and retries and backfires. Adjust empirically: raise it, watch for ThrottlingException growth in CloudTrail, and back off if it appears.
-target is frequently misused as a performance tool. Narrowing to specific resources does shrink the refresh scope, but the option’s real effect is producing a state where only part of the dependency graph has been applied. A routine built on -target drifts toward a stack where nobody knows when full consistency was last verified. Keep it as an isolation tool for emergencies, not an answer to everyday slowness.
CI offers one more structural option: separate the PR-triggered plan from a scheduled full refresh. PR plans run fast on the trust established by the last scheduled run, while a nightly full refresh catches drift once a day.
The structural fix: reading the split signals #
Options relieve symptoms; for a stack whose resource count is the problem, the fix is splitting. When these signals stack up, it is time.
- Plan crosses five minutes: the bar is when waiting for plan starts interfering with the PR review loop itself.
- State passes several hundred resources: not an absolute rule, but past that point refresh time and lock contention grow together.
- Different change frequencies share one stack: an app tier changing several times a day and a network tier changing a few times a year in one state means every app deploy refreshes the network — waste built into the structure.
- Lock waits become routine: when several people and pipelines regularly collide on state lock contention, it is a collaboration problem as much as a performance one.
Split criteria (by tier, change frequency, blast radius) and cross-stack references are covered in Ops #5, structuring at scale. The performance framing to add: splitting cuts plan time in proportion to resource count, and just as importantly redistributes refresh frequency so only the frequently changing stack refreshes frequently.
Summary #
- Plan time is refresh time: resource count times API round trips, degrading nonlinearly once rate limits engage.
- -refresh=false belongs to the development repeat loop only. Final pre-apply and PR plans do not skip refresh.
- Tune parallelism empirically; raising it on an already-throttled stack backfires.
- -target is an isolation tool, not a performance tool. Routine use normalizes partially applied state.
- Plan over five minutes, hundreds of resources, mixed change frequencies, and routine lock contention together mean the answer is a stack split, not another option.