Skip to content
Infrastructure as Code iac patterns 4 min read

Managing Large State

As infrastructure grows, a single Terraform state file that tracks every VPC, database, queue, and IAM policy becomes a liability. Plans slow to a crawl, every apply risks touching unrelated resources, and concurrent work blocks behind a single state lock. The standard answer is to split one monolithic state into several smaller states organized by component or layer, and wire them together with remote state. This page covers when and how to split, the blast-radius benefits, the cost of going too granular, and how to keep it all maintainable.

Why a single state stops scaling

Terraform reads the entire state and refreshes every resource on each plan, so refresh time grows roughly linearly with resource count. A state holding a few thousand objects can take minutes just to refresh against the provider API. Worse, the whole state sits behind one lock — if a teammate is applying, you wait.

The deeper problem is blast radius. When everything lives in one state, a careless change, a provider bug, or a botched terraform state rm can ripple across your entire estate. Splitting state contains the damage: a mistake in the monitoring state can never accidentally destroy your production database, because that database simply is not in the same state.

A practical signal that a state is too big: terraform plan regularly takes longer than a coffee refill, or two people can never work on infrastructure at the same time without colliding on the lock.

Splitting by layer vs. by component

There are two common axes for splitting, and most mature setups combine them.

StrategySplits alongExample statesBest for
LayeredLifecycle / rate of changenetwork, platform, appsSeparating slow-moving foundations from fast-moving app infra
ComponentBounded service or domainpayments, search, analyticsTeam ownership and independent deploys
Per-environmentdev / staging / prodprod/network, dev/networkStrong isolation and separate credentials

A typical layout combines all three: each environment gets its own backend prefix, and within it, state is divided into layers, then into components for the busy ones.

infra/
├── prod/
│   ├── 00-network/      # VPC, subnets, transit gateway  (rarely changes)
│   ├── 10-platform/     # EKS, RDS, shared IAM           (changes monthly)
│   └── 20-apps/
│       ├── payments/    # owned by the payments team
│       └── search/      # owned by the search team
└── dev/
    └── ...same shape

Wiring states together with remote state

Once state is split, downstream layers need outputs from upstream ones — the app layer needs the VPC ID and subnet IDs produced by the network layer. The canonical tool is the terraform_remote_state data source, which reads another state’s outputs read-only.

The upstream 00-network layer exports what consumers need:

# prod/00-network/outputs.tf
output "vpc_id" {
  value = aws_vpc.main.id
}

output "private_subnet_ids" {
  value = aws_subnet.private[*].id
}

The downstream 10-platform layer pulls them in:

# prod/10-platform/main.tf
data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "acme-tf-state"
    key    = "prod/00-network/terraform.tfstate"
    region = "us-east-1"
  }
}

resource "aws_eks_cluster" "main" {
  name     = "prod-platform"
  role_arn = aws_iam_role.eks.arn

  vpc_config {
    subnet_ids = data.terraform_remote_state.network.private_subnet_ids
  }
}

This works identically under OpenTofu, which supports the same terraform_remote_state data source and S3 backend.

Prefer data sources (e.g. aws_vpc, aws_ssm_parameter) or a published value in SSM Parameter Store over terraform_remote_state when you can. Remote state couples the consumer to the producer’s internal output names and its backend layout; a tag-based data "aws_vpc" lookup is far more decoupled.

A loosely coupled alternative publishes shared facts to SSM and reads them back:

# producer: prod/00-network
resource "aws_ssm_parameter" "vpc_id" {
  name  = "/prod/network/vpc_id"
  type  = "String"
  value = aws_vpc.main.id
}

# consumer: any downstream layer
data "aws_ssm_parameter" "vpc_id" {
  name = "/prod/network/vpc_id"
}

Migrating resources between states

Splitting an existing monolith means moving resources without recreating them. Use terraform state mv with the -state-out flag, or — cleaner in 1.5+ — the moved/removed blocks combined with re-import.

# Pull both states locally, move the resource, push back
terraform state mv \
  -state=monolith.tfstate \
  -state-out=network.tfstate \
  aws_vpc.main aws_vpc.main

Output:

Move "aws_vpc.main" to "aws_vpc.main"
Successfully moved 1 object(s).

After moving, run terraform plan in both states; each should report no changes, confirming the resource is tracked in exactly one place.

The cost of too many small states

Splitting is not free. Each additional state adds friction, and over-splitting trades one problem for several.

Trade-offMonolithMany small states
Plan/apply speedSlowFast per state
Blast radiusLargeSmall
Cross-state changesOne applyOrdered, multi-apply
Cognitive overheadLowHigh
Lock contentionHighLow

A change spanning three layers now needs three coordinated applies in dependency order, and a tooling layer (Terragrunt, a Makefile, or a CI pipeline) to orchestrate them. Aim for the coarsest split that keeps plans fast and ownership clear — not the finest possible.

Best Practices

  • Split first along lifecycle (network vs. platform vs. apps), then by team ownership for high-churn components.
  • Keep upstream-to-downstream dependencies acyclic; never let two states read each other’s remote state.
  • Prefer SSM parameters or tag-based data lookups over terraform_remote_state to decouple producers from consumers.
  • Give every state its own backend key and enable state locking (DynamoDB for S3, or native locking in OpenTofu/newer backends).
  • After any split or migration, prove correctness by running plan in every affected state and confirming zero unexpected changes.
  • Resist over-splitting — each new state adds orchestration cost; merge states that always change together.
Last updated June 14, 2026
Was this helpful?