How to split the Terraform state file (and why you shouldn’t)
Splitting a Terraform state file is treated like a rite of passage, the thing every team eventually does once their state gets "too big." Often, however, it isn't a fix; it's a trade: one coordination problem becomes several smaller ones, plus a permanent tax on every reference that now has to cross a state boundary.
"Just split the state" is the default advice the moment a Terraform configuration gets too big for a team to manage effectively. However, the ideal state is one you don't ever have to split.
This guide covers the real reasons teams split Terraform state, not just because "it got big," and every method available to split state.
We also cover what a split state architecture costs you on the other side of the migration, once the resources have moved and the dust has settled. Then, we share the actual alternative to splitting state.
What splitting a Terraform state file means
A Terraform state file maps one root module's resources to the real infrastructure it manages: instance IDs, ARNs, security group rules, the dependency metadata Terraform needs to know what depends on what.
Splitting state is the act of dividing those resources across two or more state files, each tied to its own backend and managed as an independent root module going forward.
Once you split, a terraform plan in one directory has no visibility into the other; anything shared between them has to be passed explicitly, usually through a remote state data source.
The term sometimes gets conflated with Terraform workspaces, which is a different mechanism. Workspaces let one configuration manage multiple named instances of the same state (including dev, staging, and prod) without changing the underlying resource definitions.
Splitting state changes the actual configuration itself: different .tf files, different resource blocks, and a genuinely separate Terraform root module.
Read on to learn why and how teams split Terraform state, and, just as importantly, what it costs them once the migration is done.
Five reasons engineers split a Terraform state file
A few of these reasons describe organizational facts that don't change no matter what your storage engine looks like. A few describe symptoms of the state file itself, symptoms that splitting only partially treats.
Plans and applies get slow
Every terraform plan refreshes the entire state before it calculates a diff, even when your change touches a single resource. Once a state holds thousands of resources, that refresh dominates the runtime of the command, and a one-line security group change starts taking as long as a full infrastructure review.
Blast radius grows with the state
Every resource in a single state file shares the same state lock and the same potential for a bad apply to reach it. Put your production database and a throwaway dev bucket in the same state, and a mistyped resource address in the wrong terminal window can affect both.
Teams don't want to share a lock
When the networking team and the application team both run Terraform against the same state, one team's apply blocks the other's, regardless of whether the two changes touch overlapping resources. Splitting by ownership gives each team its own lock and its own release cadence.
Compliance draws a hard line
Regulated workloads – such as PCI, GDPR, and HIPAA – sometimes need provable separation from everything else, down to the storage layer. A shared state file that holds both regulated and unregulated resources makes that separation harder to demonstrate to an auditor, even if access controls are otherwise secure.
Fast-changing resources sit next to slow-changing ones
A set of compute resources, autoscaling groups, feature-flagged instances, might change several times a day. A networking configuration, a VPC or a DNS zone, might not change for months. Sharing a state means every fast-moving change refreshes and re-evaluates the slow-moving resources too, for no benefit.
There is a clear split in that list:
- Team ownership and compliance boundaries are organizational decisions; they'd be true regardless of how Terraform stored state.
- Slow plans, blast radius, and lifecycle mismatch are symptoms of a state file that has to lock and refresh as a single, indivisible unit, regardless of how small the actual change is.
Keep that distinction in mind.
Three ways to split a state file
The legacy way: terraform state mv
Create a new directory for the destination configuration, pull the original state locally with terraform state pull, then run terraform state mv with the -state and -state-out flags to move resources from one state file to another:
Moving an entire module carries every resource inside it along automatically, which is one reason teams try to draw split boundaries around module edges rather than individual resources.
If a resource shouldn't exist in either state going forward, terraform state rm removes it from state without touching the real infrastructure, which is distinct from moving it somewhere else.
Once the move is done, you can push both files back to their respective remote backends with terraform state push and confirm a clean plan on each side, no changes proposed, before you touch the .tf files.
The current recommendation: removed and import blocks
As of Terraform 1.7, HashiCorp's own guidance favors a configuration-driven approach over the bare CLI transaction above.
In the source configuration, replace the resource block with a removed block that sets destroy = false, which drops the resource from state without touching the real infrastructure.
The destroy argument sits inside a required lifecycle block rather than at the top level of the removed block:
In the destination configuration, add the resource block back alongside an import block that references its existing ID:
This method leaves a record in version control of exactly what moved and when, which a raw state mv transaction doesn't.
HashiCorp now recommends it for new migrations, even though state mv still works and remains faster for a one-off, undocumented move.
Either way, you're importing existing resources into their new home rather than recreating them, meaning the real infrastructure never changes, only which Terraform code manages it.
Scripting the move at scale
Neither of the methods above scales cleanly to real-world splits, with each direction failing differently.
Dividing a state evenly into several new ones (networking here, compute there) usually means writing a script that loops over a list of resource addresses and runs state mv or generates removed/import pairs for each.
Moving the opposite case, a handful of resources out of a state that holds thousands of others, is where the block-based method gets misapplied. You don't need a removed block for every resource staying behind, only for the ones actually moving.
Confusing the two turns a nine-resource migration into a twelve-hundred-line configuration change, which is unnecessary and worth catching before you start writing blocks by hand.
Whichever method you use, save a backup of the original state before you start, and verify with terraform plan on both the source and the destination directory afterward. A clean plan and an apply complete with zero changes on both sides is the only real confirmation that resources moved correctly and nothing was left half-migrated.
What splitting costs you
Every reference that used to be a local resource address inside one state now has to cross a state boundary, and the standard tool for that, a terraform_remote_state data source, reads a producer's entire state file to hand a consumer a single output value.
Reference one VPC ID out of fifty exported outputs, and your plan still pays the cost of fetching, transmitting, and parsing every other output the producer state contains, including any that were meant to stay internal.
Coordination doesn't disappear when you split a state; it multiplies. One state equals one lock and one thing that can block a release. Five states equal five locks, five things that can independently fail a pipeline, and an apply order between them that Terraform no longer tracks for you.
Someone (a human, a runbook, or a wrapper tool like Terragrunt orchestrating dependencies externally) now has to know that the networking state applies before the compute state that reads its outputs; Terraform's own dependency graph stops at the state boundary you just drew and doesn't track that ordering on your behalf anymore.
Splits also tend not to just stay split once. A monolithic state file that gets carved into a "networking" state for blast-radius reasons often grows until that state itself needs splitting into yet more different state files: VPCs separated from subnets, subnets separated from peering connections.
The underlying cause (a flat file that locks and refreshes as a single unit, no matter how targeted the change) hasn't gone anywhere. It's just been pushed one level down, into a smaller version of the same constraint.
And every new state is a new backend configuration, a new set of IAM permissions scoped to it, and another line in the CI pipeline that has to plan and apply it in the right sequence.
When splitting is the right call, and when it isn't
Team-ownership and compliance-driven splits make sense because they're organizational decisions that are infrastructure-first. A regulator doesn't care whether your Terraform backend is fast; they care whether the regulated workload is provably isolated.
A platform team that owns networking shouldn't have to queue behind an application team's release, regardless of how quickly either state plans. Split for those reasons and you're solving an actual problem.
Performance and blast radius are a different case. Slow plans and an oversized blast radius aren't caused by having "too many resources" in the abstract; they're caused by a storage model that has to lock and refresh an entire state file for any change, however small.
Instead of removing that constraint, splitting that state into five smaller files just applies it to five smaller groups of resources rather than one large one, handing you the cross-state reference tax and the coordination overhead described above as the price of admission.
Terraform state is a coordination problem stored as a JSON file, one usually better solved with finer-grained locking, isolating exactly the resources a given operation touches, than with drawing more file boundaries around groups of resources and hoping the boundaries line up with how your infrastructure actually changes.
A better fix than splitting state
If the reason on your list is team ownership or compliance, split the state; that's the right solution for that problem. If the reason is plan speed, lock contention, or blast radius, the more durable fix is removing the reason to split rather than splitting again.
Stategraph approaches this by locking and refreshing at the resource level instead of the whole state file, so an operation on twelve resources doesn't wait behind, or block, operations on the other several thousand.
Where a terraform_remote_state data source pulls a producer's entire state to expose one output, Stategraph resolves cross-state reads at the field level: reference one output, and only that output moves, while the rest of the producer's state stays isolated.
Teams still get the isolation a split was meant to provide, without taking on a new backend, a new IAM boundary, and a new entry in the CI pipeline for every resource group that used to share a lock.
We're not saying state should never be divided along real organizational lines; sometimes it should. We're saying you shouldn't let a storage limitation force you into an architecture decision you wouldn't otherwise make, and don't create a new backend and a new pipeline stage to simplify a constraint that finer-grained locking would have handled on its own.
Conclusion
Splitting a Terraform state file is sometimes the right decision: team boundaries and compliance requirements are organizational facts that no amount of better tooling changes. However, often, it's a workaround that feels like a fix to slow plans and an oversized blast radius, which are symptoms of a state file that locks and refreshes as a single indivisible unit.
Instead of removing the constraint, dividing that file into several smaller indivisible units just multiplies the coordination overhead you're managing by hand. Before you migrate a single resource, work out which of the two problems you're actually looking to solve.
If it's the second one, Stategraph can handle resource-level locking and field-level cross-state reads without the backend sprawl a traditional split takes on. Try Stategraph free and run a plan against your current state to see what changes in refresh time alone.
Terraform split state file FAQs
Is it bad practice to have one large Terraform state file?
Not inherently. Size alone isn't the problem; what's inside the state and who has to touch it are. A state file with two thousand resources that one team owns and rarely works on concurrently causes far less friction than a five-hundred-resource state shared across three teams with conflicting release schedules. Diagnose the actual friction (is it lock contention? Blast radius? Plan time?) before assuming size is the root cause.
What's the difference between terraform state mv and using removed and import blocks?
terraform state mv is a direct CLI transaction: it moves a resource between two local state files with no record in your configuration that a move happened.
removed and import blocks get you to the same result but leave that record in version control, which is why HashiCorp's own documentation now recommends them for new migrations on Terraform 1.7 and later. state mv still works and remains the faster option for a quick, well-understood, one-off move.
Should I split my Terraform state by environment or by service?
There's no universal rule, and it depends on which axis of change and ownership actually causes friction for your team. If dev, staging, and production get worked on by different people on different schedules, split by environment. If separate teams each own a distinct service or infrastructure layer, split by service or component instead.
Some organizations end up doing both, splitting by environment first and then by service within each one, which is exactly the kind of compounding split to watch out for.