One codebase, two projects
OpenTofu began as a fork of the last MPL-licensed Terraform release line (1.5.x) after HashiCorp moved Terraform 1.6 to the Business Source License. The fork was initiated by companies that build on Terraform (Gruntwork, Spacelift, Harness, Env0, Scalr and others, per the OpenTofu FAQ) and handed to the Linux Foundation. That origin explains most of what follows: OpenTofu exists to keep an MPL-licensed engine for the vendor ecosystem, so it prioritises compatibility with Terraform configurations and the shared provider ecosystem, and it has since added features of its own (client-side state encryption, provider iteration, .tofu overrides).
The two still share the language, the plan-and-apply workflow, the state file format and the providers. OpenTofu has no providers of its own; it resolves them from the OpenTofu Registry, which mirrors the public provider and module ecosystem. What they do not share any more is ownership, release cadence, and a growing list of features on each side.
Licensing: who is actually restricted
This is the question most teams ask first and answer wrong. Read the licence text rather than the commentary. Terraform 1.6.0 and later are licensed under BUSL 1.1 with an Additional Use Grant: you may make production use of Terraform "provided Your use does not include offering the Licensed Work to third parties on a hosted or embedded basis in order to compete with" IBM's paid versions. The same text says that "hosting or using the Licensed Work(s) for internal purposes within an organization is not considered a competitive offering", and that products "not provided on a paid basis are not competitive". Each release converts to MPL 2.0 four years after it is published.
So for an engineering team running Terraform against its own cloud accounts, in its own CI, the licence changes nothing in practice. The restriction bites vendors: a paid CI/CD, platform or managed-Terraform product that wraps the binary is what the Additional Use Grant is written for. OpenTofu is plain MPL 2.0, with no use grant to interpret, which is why the platforms that started the fork ship it. That is a fact about the licence, not a judgement about the companies.
Version context as of September 2026
Terraform is at v1.16.4 (released 23 September 2026) and OpenTofu at v1.12.6 (19 August 2026), per each project's GitHub releases page on the verification date. Terraform 1.16 is documented as "a minor release in the stable Terraform v1.0 series" that honours the v1.0 compatibility promises. OpenTofu's documentation site was publishing 1.12.x as current with 1.13 in beta. The numbers are not comparable across projects; OpenTofu continued Terraform's 1.x numbering from 1.6.0 when it forked and has shipped major features on its own schedule since.
Where the two have diverged
| Capability | Terraform (1.16) | OpenTofu (1.12) | Why it matters |
|---|---|---|---|
| S3 native locking | use_lockfile, GA in 1.11; DynamoDB arguments deprecated | use_lockfile preferred; DynamoDB "fully supported", no deprecation planned | Decides whether your lock table has a deadline |
| State and plan encryption | Backend-side only (HCP Terraform, S3 encrypt, GCS keys) | Client-side encryption block; unrecoverable without the key | Threat model for a leaked state object |
| Ephemeral values / write-only args | Yes (1.10 / 1.11) | Yes (ephemeral variables, resources, write-only attributes) | Keeps secrets out of state on both |
| provider for_each | No; provider block takes alias only | Yes, on aliased providers | Multi-region and multi-account modules |
| File extension | .tf | .tf and .tofu (.tofu wins on a name clash) | Shipping tool-specific files in shared modules |
| Variables in backend config | No; a backend block cannot refer to input variables, locals or data sources | Yes; variables and locals that resolve during tofu init | Per-environment state location without -backend-config files or wrapper scripts |
| Excluding resources from a run | -target only | -exclude and -exclude-file, plus -target | Recovery runs that skip one broken resource |
| Hosted platform | HCP Terraform, Terraform Enterprise, Stacks (GA) | None first-party; third-party platforms | Whether a vendor runs your applies |
Two of those rows are the ones that change decisions: locking policy and encryption. The rest are conveniences you will notice only if you hit them.
Compatibility and the migration you would actually run
OpenTofu's own guidance is careful: it "aims to maintain compatibility with Terraform configurations" and "most Terraform code will work without modification", and the FAQ states it "will work with existing state files up to those created with Terraform versions 1.5.x". Read the second sentence literally: the promise covers state created by Terraform up to 1.5.x and says nothing about state written by 1.6 or later. Our reading, not a documented guarantee, is that a migration is safest from a state file a 1.5.x-era Terraform could still read, and that it becomes a one-way door once OpenTofu-only features (encryption, .tofu files, provider iteration) touch the code or the state.
The documented migration is short and reversible as long as you keep the backups it tells you to take: back up state and code, install OpenTofu, run tofu init, run tofu plan and expect "No changes", then tofu apply so the state is written by OpenTofu, then make one small change end to end. Rolling back means restoring the backup and running terraform init and plan again. In a CI-driven estate this is a branch, a pinned binary, and a plan you can diff.
cp terraform.tfstate terraform.tfstate.pre-tofu # or: rely on S3 bucket versioningtofu initInitializing the backend...Initializing provider plugins...OpenTofu has been successfully initialized!tofu planNo changes. Your infrastructure matches the configuration.# anything other than "No changes" here: stop, do not apply, read the diffThe transcript above is the shape the migration guide tells you to expect, not a recording from a specific run; the exact provider lines depend on your configuration. The decision value is in the last line: OpenTofu's documented success criterion is a plan with no changes, and its documented failure response is "do not apply".
S3 state locking: the one place the advice is different
Both tools now implement native S3 locking through a .tflock object next to the state, enabled with use_lockfile = true. The policies around it differ, and this is where teams copy the wrong advice across the fence.
Terraform: the S3 backend reference marks dynamodb_table as deprecated, says DynamoDB-based locking "will be removed in a future minor version", and allows both mechanisms to be configured at once "to support migration from older versions of Terraform that only support DynamoDB-based locking". The 1.11 upgrade guide made S3 native locking generally available and deprecated the DynamoDB arguments. The lock file needs s3:GetObject, s3:PutObject and s3:DeleteObject on the .tflock key, and Terraform does not need s3:DeleteObject on the state object itself.
OpenTofu: the S3 backend reference calls native S3 locking "the preferred one" and DynamoDB "another option", and then states in a note that "both S3 and DynamoDB locking mechanisms are fully supported, and the OpenTofu team has no plans to deprecate either option". When both are configured, OpenTofu documents the semantics precisely: it acquires the S3 lock first, then the DynamoDB lock, and considers the lock held only when both succeed.
terraform {backend "s3" {bucket = "acme-tfstate"key = "prod/network/terraform.tfstate"region = "eu-west-1"encrypt = trueuse_lockfile = true# Terraform: keep dynamodb_table ONLY during the migration window.# The argument is deprecated and scheduled for removal in a minor release.# OpenTofu: keeping it is a supported dual-lock (S3 first, then DynamoDB,# lock held only when both succeed). Remove it when you no longer want it,# not because a deprecation forces you to.# dynamodb_table = "acme-tf-locks"}}
Practical consequence: on Terraform, plan to retire the DynamoDB table; on OpenTofu, the table is a choice you can keep. If a repository is used by both tools (a Terragrunt estate mid-migration, for example), write the backend for the stricter tool and note which one owns the schedule.
State encryption and secrets in state
Both tools write secrets into state in plain text unless you stop them, and both now offer ephemeral values that never reach state: Terraform since 1.10 (with write-only resource arguments in 1.11), OpenTofu with ephemeral variables, resources, outputs and write-only attributes in its 1.12 documentation. For the secrets that still land in state, the tools part ways.
Terraform's answer is the backend: HCP Terraform "automatically encrypts state at rest", the S3 backend encrypts at rest when encrypt is enabled, GCS supports customer-managed keys. The state object is therefore readable by anyone with read access to the bucket and the key. OpenTofu adds a second layer: an encryption block that encrypts state and plan files on the client before they reach any backend, keyed by a passphrase or a KMS provider (the AWS KMS example in the docs is three lines), with a fallback block for rotating keys. The docs are blunt about the cost: "when you enable encryption, your state and plan files become unrecoverable without the appropriate encryption key", and OpenTofu "will refuse to read plain text data" once encryption is on, so you must follow the documented migration steps and rehearse recovery first.
terraform {encryption {key_provider "aws_kms" "state" {kms_key_id = "a4f791e1-0d46-4c8e-b489-917e0bec05ef"region = "eu-west-1"key_spec = "AES_256"}method "aes_gcm" "state" {keys = key_provider.aws_kms.state}state { method = method.aes_gcm.state }plan { method = method.aes_gcm.state }}}
Language and workflow differences that change how you write modules
provider for_each is the clearest one. OpenTofu lets an aliased provider block carry for_each, so a module can instantiate one provider per region or account from a map. Terraform's provider block, per its reference, takes alias (and a deprecated version argument) and nothing else, so the same design is done with one alias per region, written out by hand or generated. This is an inference from the two references rather than a benchmark, but it is the kind of thing that shows up as fifty near-identical provider blocks in a real multi-account repo.
The .tofu file extension is the other one worth knowing. OpenTofu loads .tf and .tofu files and, when both exist with the same base name, "will prioritize the .tofu file and ignore the .tf file". A shared module can therefore carry a Terraform-compatible file and an OpenTofu-specific override side by side, which is the mechanism for adopting OpenTofu features without breaking Terraform consumers of the module.
The day-to-day workflow is otherwise the same on both: init, plan, apply, the same providers, the same state format. The command name changes from terraform to tofu and CI images change; the muscle memory does not.
The platform around the binary
Terraform is one half of a product. HCP Terraform and Terraform Enterprise host the runs, encrypt state at rest, and are where Stacks live; the documentation describes Stacks as "a powerful configuration layer in HCP Terraform" and documents their generally available (GA) release. If those are load-bearing for you, the binary choice is made for you: Stacks and the HCP run model are Terraform features.
OpenTofu has no first-party platform. Its platform story is the third-party ecosystem that created it, plus plain CI. For a team already running plans and applies in GitLab or GitHub pipelines with an S3 or GCS backend, that is not a gap; for a team that bought HCP Terraform for its workspace model and policy sets, it is the whole decision.
Five situations, five answers
Scenario recommendations
| Situation | Run | Why | Primary trade-off |
|---|---|---|---|
| Existing Terraform estate, already on 1.6+, internal use only, no HCP dependency | Stay on Terraform for now; keep the OpenTofu migration rehearsed | The licence does not restrict you and the estate is past the 1.5.x state line the FAQ promises compatibility for; a forced migration buys nothing today | You inherit the DynamoDB deprecation schedule and any future licence change |
| Greenfield, open-source-first team, CI-driven applies, S3/GCS backend | OpenTofu | MPL 2.0 with no use grant to interpret, client-side state encryption, provider for_each; nothing in the HCP platform is being given up | Smaller vendor support market; .tofu features are a one-way door |
| Regulated enterprise that wants a support contract and a hosted control plane | Terraform with HCP Terraform or Enterprise | The requirement is the platform (hosted runs, state encrypted at rest, Stacks), and that platform runs Terraform | Vendor lock-in is the point; budget accordingly |
| Team dependent on Stacks or other HCP Terraform / Terraform Enterprise features | Terraform | Those are Terraform-only features by definition | Same as above |
| Vendor or platform team offering IaC runs to paying customers | OpenTofu | The BUSL Additional Use Grant is written for exactly this case; OpenTofu removes the question | You own the migration and the support |
| Organisation evaluating migration risk but not forced to move | Run the documented dry run on one non-critical stack and keep both binaries pinned | A tofu plan showing "No changes" against a backed-up state is the cheapest evidence you can buy | Two toolchains to keep patched until you decide |
The trade-offs, stated with their costs
Licence clarity. Benefit: OpenTofu removes a legal interpretation from the roadmap. Cost: you leave the project with the larger commercial support market and the first-party platform. Who cares: vendors and platform teams first, internal teams barely.
Client-side state encryption. Benefit: a leaked state object is ciphertext. Cost: key custody becomes a production dependency, and the docs warn the state is unrecoverable without the key. Who cares: teams whose state leaves the bucket, and anyone who has watched a state file end up in a build artifact.
Locking policy. Benefit on Terraform: a clear push toward one mechanism. Cost: a migration you did not schedule. Benefit on OpenTofu: no forced change. Cost: two supported mechanisms means two things to document. Who cares: anyone with a DynamoDB table and more than a handful of stacks.
Feature velocity. Benefit: OpenTofu ships community-requested features (provider iteration, client-side encryption, .tofu overrides). Cost: each one you adopt widens the gap to Terraform and narrows your exit. Benefit on Terraform: Stacks and a hosted platform. Cost: the best of it is behind HCP. Who cares: module authors who publish to both audiences, and anyone planning to keep the option to switch.
Performance. There is no first-party benchmark comparing the two, and this page ran none. Both tools execute the same providers, which is where most of the wall-clock time goes, so a performance argument for either would need measurement we do not have.
What we would run, and why
For a new platform team with a CI-driven workflow and no HCP contract: OpenTofu. The licence is unambiguous, state encryption is a feature we would actually turn on for production state, and the migration guide's rollback path means the choice is cheap to reverse early. That is a recommendation, and the evidence behind it is the licence text and the encryption reference, not a production war story.
For an estate that is already on Terraform 1.6 or later and uses only the open-source CLI internally: stay, but rehearse. The licence does not touch internal use, moving state across the 1.5.x line is outside OpenTofu's stated compatibility promise, and the concrete thing you must do on Terraform is retire DynamoDB locking on a schedule you control. Keep the migration dry run in a runbook so the decision remains yours.
For anyone who bought or needs HCP Terraform, Terraform Enterprise or Stacks: Terraform, without agonising. The engine choice follows the platform. The one thing we would not do in any of these cases is decide based on a feature table alone; run the plan against a backup and read what comes out.