CostCenter and costcenter are two different tags, and the history never comes back

30 July 2026

There is a tool for this on the site

Two teams tag their resources. One uses CostCenter. The other, following a runbook someone typed from memory, uses costcenter.

Both are valid. Both are applied. Both appear in cost allocation as separate dimensions. And every report grouped by CostCenter silently excludes everything the second team owns.

Why it does not look like anything

The failure mode here is unusually quiet, and the reason is worth being precise about.

The grand total is correct. Every dollar is accounted for; it is just distributed across two columns instead of one. There is no discrepancy to notice, no reconciliation that fails, no alert, and no sum that does not add up.

A cost report showing CostCenter: platform at $40,000 looks exactly like a cost report showing CostCenter: platform at $40,000, whether or not another $12,000 sits under costcenter: platform a few rows down. If the second key is not in your chosen grouping, its rows are simply not there.

It surfaces months later, usually as a team saying their spend does not match what they know they are running. By then the question is not how to fix the tag.

The history is the expensive part

Cost allocation data is not restated. Activating a tag key applies from that point forward; costs already incurred are not broken down retroactively.

So a split that ran for six months stays split in the history forever. You can retag every resource today and the reports for those six months remain unusable, which means any year-over-year comparison crossing the boundary is comparing two different measurements.

This is why the fix is worth doing before the tags are in use, and why “we will clean it up later” is a different kind of promise from usual. Later does not recover the data.

Applying a tag is not activating it

A separate failure with the same silence, and the two frequently arrive together.

A perfectly tagged estate produces nothing in cost reports until each key is activated as a cost allocation tag. That is a separate action, in a different console, in the management account, by someone who may not be the person who wrote the policy.

The tags are on the resources. The policy is enforced. The compliance report is green. And Cost Explorer has no such dimension.

Since activation is not retroactive either, every day between rollout and activation is a day of history you cannot analyse. Activate the key when you define it, before the rollout, not after. The ordering of those two steps matters more than either one individually.

A policy without enforced_for prevents nothing

The third thing people configure and then misread:

{
  "tags": {
    "owner": { "tag_key": { "@@assign": "Owner" } }
  }
}

This is a valid tag policy. It does not stop anybody creating an untagged resource.

Without enforced_for, a tag policy produces a compliance report: non-compliant resources are created successfully and appear in a list afterwards. That is a reasonable first step and it is not what most people believe they just configured.

Two further details that catch people:

enforced_for constrains tag values, not tag presence. It prevents a non-compliant value on the listed resource types. Without a tag_value allow-list beside it there is nothing to be non-compliant with, so enforcement has no effect at all.

It does not require the tag. A resource created with no Owner tag is not violating a policy that governs what Owner may contain. Requiring presence is a different mechanism: an SCP with a Null condition on the tag key, or an IaC policy check at deploy time.

That distinction accounts for a lot of “we have tag policies and half the estate is untagged”.

Values are case-sensitive too

The same problem one level down, and it can be baked into the policy itself:

"tag_value": { "@@assign": ["prod", "Prod", "staging"] }

Permitting both capitalisations splits the dimension exactly as a key collision does, with the blessing of the allow-list that existed to prevent it.

Trailing whitespace does the same thing and is worse, because "prod " and "prod" are visually identical in every console and every CSV export. Nobody finds that by looking.

Pick one form. Lowercase is the usual choice, mostly because it is the only one nobody argues about.

The wrong instinct: fix it in the report

When the split is discovered, the tempting fix is a mapping in the BI layer: alias costcenter to CostCenter and move on. It works, and it is a trap for three reasons.

It has to be maintained forever, by whoever inherits the dashboard. It only fixes the reports that go through that layer, so Cost Explorer, budgets, anomaly detection and the raw CUR export all still show the split. And it removes the pressure to fix the underlying tags, so the estate keeps drifting.

Fix the tags. Use the report mapping as a stopgap during the migration, with a date on it.

What changed recently

Tag policies can now require tags on resource creation in more places than they once could, but the belt-and-braces answer is still an SCP with a Null condition, or policy-as-code in the pipeline. Catching it at terraform plan is better than catching it at creation, which is better than catching it in a report.

Data exports replaced the older cost and usage report format. If you have tooling reading the legacy CUR, the migration is also the natural moment to audit which tag columns you are actually consuming, because unused activated tags carry no cost but do add columns.

Resource Explorer and the tag editor make a bulk audit practical without writing scripts, which is worth knowing before anybody starts building one.

Fixing an estate that has already drifted

1. Find the collisions before deciding anything. Group every distinct tag key by its lowercase form. Any group with more than one member is a split. Do the same for values within a key.

2. Pick the canonical form and write it down somewhere that is not a runbook nobody reads. The policy document is the right place.

3. Activate the canonical key first, before retagging, so that the moment resources start carrying it the data begins accumulating.

4. Retag in bulk, then verify the old key is gone rather than assuming. Resource Explorer or the tag editor will do this without scripting for most resource types; the exceptions are usually in services with their own tagging APIs.

5. Add enforced_for with a tag_value allow-list only once the estate complies. Enforcing before that blocks legitimate work and produces an exception process that outlives the policy.

6. Add a presence check separately, in the pipeline or via SCP, because the tag policy will not do it.

When this does not matter much

A single-account estate with one team does not really have an attribution problem. The tags are still worth having for lifecycle and automation, and the case-collision risk is proportionally smaller because fewer hands are creating resources.

If you allocate cost by account rather than by tag, which is a legitimate and much simpler model, the entire tag-splitting problem does not arise. Account boundaries cannot be misspelled. The trade-off is granularity and the overhead of managing many accounts.

If nothing consumes the tag, an inconsistent one is untidy rather than expensive. Worth checking before a large retagging project: some estates carry tags that were required by a policy nobody remembers writing and that no report has ever grouped by.

The short version

  • Tag keys and values are case-sensitive. CostCenter and costcenter are two tags.
  • The grand total stays correct, so nothing looks wrong. That is what makes it survive.
  • Cost allocation data is not restated. A split period stays split forever.
  • Activate the key before rolling the tag out. Activation is not retroactive either.
  • enforced_for constrains values, not presence, and does nothing without a tag_value list.
  • Requiring a tag to exist needs an SCP or a pipeline check, not a tag policy.
  • Fix the tags, not the dashboard.

Paste a policy or a plain Key=Value list into the cloud tagging policy validator and it reports case collisions across rules, case variants inside a single allow-list, whitespace you cannot see, reserved aws: prefixes, and rules defined without enforcement.