> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vantage.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Scaling Virtual Tags

> Keep virtual tag processing fast and predictable as you add providers, values, and nested allocation chains.

How long a virtual tag takes to process is a direct function of how much data it covers and how complex it is. A few configuration choices—backfill window, tag breadth, nesting depth, and virtual tag type—drive that cost, and choosing them well keeps tag changes fast as your account grows.

<Tip>
  This guide assumes you are familiar with the [Virtual Tags](/tagging) feature reference and the [tagging examples](/tagging_examples).
</Tip>

## How Vantage Processes Virtual Tags

When you save a virtual tag, Vantage reapplies it across the cost data in its scope, reprocessing each affected billing data import. A few factors determine how much work each save creates:

* **Backfill window:** Billing Period Backfill controls how far back the tag is reapplied. A multi-year backfill covers significantly more data than a few months.
* **Tag breadth:** Each value, each collapsed key, and each filter inside a tag adds to the work required to apply it. More values usually mean more work, but what matters most is how many cost rows (the individual line items in your cost data) fall in scope and, for allocations, how many targets each row splits into. As a result, a small tag that matches a huge number of rows or fans out into many allocation targets can cost more than a larger one. Complex filter operators (`flexible match`, `contains`) also take longer to evaluate than equality.
* **Nesting depth:** When one virtual tag references another, the referenced tag has to be processed first. Every additional level of nesting adds another full run over the data in scope (a "pass").
* **Virtual tag type:** Custom Value and Tag Key Collapsing tags are lightweight: they map or copy values without changing how many cost rows exist. Allocations do more work because they split each matched cost row into multiple rows, one per team, project, or other target the cost is spread across. That added row volume is the real driver. Percent-Based allocations are usually cheap because they split across only a handful of fixed percentages. Cost-Based and Business Metrics-Based allocations also aggregate a driver (spend or a business metric) and can fan each row out across many targets, so they take longest.

Reporting is mostly separate from processing: once a tag is applied, Cost Reports, segments, and saved filters read the result directly, so report performance depends largely on filter operators and provider scope. The main exception is allocations: because they split costs into many more rows, a report that groups or filters by an allocated dimension reads more data and can run slower than the same report over the same costs without that grouping. In addition, the more targets the allocation spreads across, the bigger the effect.

## Selecting the Right Virtual Tag Type

Picking the right type for each use case is the choice that has the biggest effect on processing time. The following table lists each type from fastest to slowest to process.

| Type                                                                                             | Use when                                                                                                                                                  | Processing time                                                                                                                                                                                                                      | Example                                                                                      |
| ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| [Custom Value](/tagging_examples#custom-values-examples)                                         | You are mapping existing cost dimensions (provider tags, accounts, services, resource IDs) to a standardized value.                                       | Low. One lookup per value each time the tag runs.                                                                                                                                                                                    | A `team` tag with one value per team, each scoped to that team's AWS accounts.               |
| [Tag Key Collapsing](/tagging_examples#tag-key-collapsing-examples)                              | Teams have applied semantically identical keys with different spelling or casing (`env`, `Environment`, `environment`) and you want one standardized key. | Low. One lookup per collapsed key each time the tag runs. Values copy through automatically, so you don't have to enumerate them. If a key you're collapsing is itself another virtual tag, it becomes a dependency and adds a pass. | Collapse `env`, `Environment`, and `environment` into a single `environment` virtual tag.    |
| [Percent-Based Allocation](/tagging_examples#percent-based-allocation-example)                   | The split is contractual or pre-negotiated (Marketplace purchases, AWS Shield, support fees with a fixed agreement).                                      | Low, but still an allocation. Cheap because costs split across only a few fixed percentages, not because it skips allocation work.                                                                                                   | Marketplace costs split 60/30/10 across Infrastructure, Mobile, and Data.                    |
| [Cost-Based Allocation](/tagging_examples#cost-based-allocation-example)                         | A shared cost (support fees, shared infra) should track the proportional spend of teams or projects that actually used the underlying service.            | Higher. Aggregates the input costs, then splits each matched row across every team or project it touches, so more targets mean more line items created.                                                                              | Allocate the monthly AWS Support fee across teams using the `team` tag's share of AWS usage. |
| [Business Metrics-Based Allocation](/tagging_examples#business-metrics-based-allocation-example) | You have a real external driver (CPU seconds per team, seats per business unit, requests per product) and want costs to follow that driver.               | Higher. Like cost-based: aggregates the business metric, then fans each matched row out across the targets that carry that metric.                                                                                                   | Allocate a shared Kubernetes namespace's cost across teams using a labeled CPU usage metric. |

<Tip>
  When the split between teams is fixed (a negotiated chargeback ratio, a contractual percentage), use **Percent-Based** rather than Cost-Based. Same result, much less processing.
</Tip>

## Recommended Hierarchies for Large Organizations

The hierarchies that scale best are flat and intentional. Add nesting only when the relationship is dynamic, that is, when an underlying mapping changes often enough that maintaining it in one place is worth a serial processing pass.

### Start with a Flat Core Set of Tags

For most organizations, a small core set of five flat virtual tag keys covers most reporting needs. Keep each one independent, with no nesting between them, so they don't depend on each other and edits don't cascade across tags.

```mermaid theme={null}
flowchart LR
    costs[Provider costs] --> environment["🏷 environment<br/>(prod, staging, dev)"]
    costs --> team["🏷 team<br/>(infra, mobile, data, ...)"]
    costs --> costCenter["🏷 cost-center<br/>(R&D, G&A, COGS)"]
    costs --> application["🏷 application<br/>(checkout, search, ...)"]
    costs --> businessUnit["🏷 business-unit<br/>(core, platform, ...)"]
```

Each of the above tags is a Custom Value tag (or a Tag Key Collapsing tag if your underlying provider tags have casing variations). Build the core set first and resist adding more until you have evidence that a sixth key is needed.

### Nest Only When the Mapping Is Dynamic

The classic case for nesting is `application` → `business-unit`. Applications change ownership across business units more often than business units appear or disappear, and you want one place to update that mapping.

```mermaid theme={null}
flowchart LR
    costs[Provider costs] --> applicationTag["🏷 Virtual Tag: application"]
    applicationTag --> businessUnitTag["🏷 Virtual Tag:<br/>business-unit<br/>(references application)"]
    businessUnitTag --> reports[Cost Reports]
```

This is a two-level hierarchy. Every time a relevant import is processed, Vantage runs an `application` pass first, then a `business-unit` pass. That is acceptable. A four-level hierarchy—`resource` → `application` → `team` → `business-unit`—usually is not, because each level adds another processing pass. This nesting limit is a performance guideline, not a hard rule. When a chain starts to feel that deep, look for a layer whose mapping is stable enough to inline into its parent.

See the [Nested Virtual Tags Example](/tagging_examples#nested-virtual-tags-example) for the full walkthrough.

### Use Allocation Chains Sparingly for Chargeback

Allocated tags can reference each other in a chain (for example, `Team` → `Sub-Team`), which is useful for layered chargeback. Unlike nesting depth, this is a hard limit: a hierarchy can be up to **three allocated tags deep**. Only allocated tags count toward it, and non-allocated tags (Custom Value, Tag Key Collapsing) don't, so a chain can be more than three levels deep overall as long as no more than three of those levels are allocations. Chaining allocations is also one of the most expensive things you can do to processing time. Each allocation in a chain expands rows on top of the one before it, and matching against an already-allocated tag costs more than matching a regular tag, so every added link multiplies the work. Avoid mixing many independent allocated tags on the same provider since each one redistributes costs into a separate dataset.

* **Allocation chains are per provider:** An AWS allocation chain is independent of a GCP one. You can have a percent-based `Team` allocation on AWS and a separate cost-based allocation on GCP with no conflict. Non-allocated virtual tags (Custom Value, Tag Key Collapsing) can also span providers freely.
* **Within a single provider, allocated tags can reference each other only when they sit in the same chain:** If you already have a `Team` percent-based allocation on AWS and want to add an `Environment` allocation that builds on it (for example, to split costs further within a team), structure `Environment` to chain off `Team`, referencing `Team = Engineering` (or similar) in its Output Costs Filter. If a new allocated tag tries to reference allocated tags from two unrelated AWS chains in one configuration, Vantage rejects the save with the error *"cannot reference tags from different allocation groups within the same provider (aws)."*

  Two independent allocated chains can still coexist on the same provider (for example, a `Team` → `Sub-Team` chain and an unrelated `Region` allocation, as in the [Allocation-on-Allocation Example](/tagging_examples#allocation-on-allocation-example)). They just can't be combined on the same Cost Report. If you try, Vantage returns *"Cannot filter or aggregate costs for allocated virtual tags that belong to separate hierarchies."* Analyze each chain in its own report.

  <Tabs>
    <Tab title="Allowed" icon="circle-check">
      ```mermaid theme={null}
      flowchart LR
          awsCostsGood[AWS costs] --> teamGood["🏷 Team<br/>(allocated)"]
          teamGood --> envGood["🏷 Environment<br/>(allocated,<br/>references Team only)"]
      ```

      A new allocated tag chains off an existing allocated tag in the same chain.
    </Tab>

    <Tab title="Rejected" icon="circle-xmark">
      ```mermaid theme={null}
      flowchart LR
          awsCostsBad[AWS costs] --> teamBad["🏷 Team<br/>(allocated)"]
          awsCostsBad --> regionBad["🏷 Region<br/>(allocated, separate chain)"]
          teamBad --> envBad["🏷 Environment<br/>(references both Team<br/>and Region)"]
          regionBad --> envBad
      ```

      A single allocated tag tries to reference allocated tags from two unrelated chains on the same provider.
    </Tab>
  </Tabs>

See the [Allocation-on-Allocation Example](/tagging_examples#allocation-on-allocation-example) for the constraints on combining allocated tags on the same report.

## Best Practices to Keep Processing Time Predictable

The following recommendations are listed roughly in order of impact. The first three have the biggest effect.

<AccordionGroup>
  <Accordion title="Scope filters to the providers that actually have the data" icon="circle-check">
    Selecting **All Providers** on a filter set is convenient, but it tells Vantage to evaluate that filter against every connected provider's data and makes every provider's processing depend on the tag keys it references. If a tag's logic is only meaningful for AWS, scope its filters to AWS.

    Use **All Providers** when the underlying logic really does apply uniformly across every provider you have or will ever connect, for example, a `team` rule that depends only on a `team` tag every provider already emits.
  </Accordion>

  <Accordion title="Set the smallest backfill window you actually need" icon="circle-check">
    Billing Period Backfill is the top-level multiplier on every tag processing run. Pick the earliest month you actually report on, not the earliest month Vantage has data for. Extending the backfill later is straightforward, but it reprocesses every import in the resulting window, not just the months you added, so it's worth setting deliberately up front.

    For a forward-looking tag (a new team, a new chargeback model that starts next quarter), set the backfill to the current month.
  </Accordion>

  <Accordion title="Keep nested chains shallow and intentional" icon="circle-check">
    Every nested level adds a serial processing pass on every relevant import. Two levels is fine; three is sometimes justified; four or more rarely pays for itself. If you find yourself building a deep chain, ask whether the deepest layer would be more accurate as a flat tag with its own filter set.
  </Accordion>

  <Accordion title="Make all your edits to a tag before saving" icon="circle-check">
    Every save reprocesses the imports in scope, and each round reapplies every tag that applies to those imports, not just the tag you changed. To avoid repeating that work, make all intended changes to a tag before saving instead of saving after each tweak. Changes to separate tags still require separate saves.
  </Accordion>

  <Accordion title="Prefer equality operators over partial matches" icon="circle-check">
    Filter operators are not equally fast:

    * `is` is an exact match Vantage can look up directly (supplying multiple values checks them as a set). Fastest.
    * `contains`, `starts with`, and `ends with` scan the text of every value in scope, so they inspect far more data.
    * `flexible match` normalizes and pattern-matches every value before comparing, which makes it the slowest.

    Use partial matches only when you need to handle inconsistent values you cannot fix upstream. If the variation is in the tag *key* (`env` vs `Environment`), use Tag Key Collapsing instead. It is fast and gives you a single canonical key.
  </Accordion>

  <Accordion title="Use Tag Key Collapsing to standardize keys, not per-value filters" icon="circle-check">
    The wrong way to standardize an `environment` tag with three key variations is to create one virtual tag value per environment with three filters each. The right way is to create one virtual tag with three collapsed keys. Collapsing copies values through automatically and stays correct as new values appear at the provider.

    Reserve per-value Custom Value filters for when the source-to-target mapping is genuinely many-to-one and would not survive a value being copied through verbatim.
  </Accordion>

  <Accordion title="Order overlapping rules so the right one wins" icon="circle-check">
    Within a virtual tag, when two rules match the same row, the rule higher in the list takes precedence. Order doesn't change how much work matching takes, but it does decide which value is applied when rules overlap. Put more specific rules above broader ones so the value you actually want is the one that lands on the row.
  </Accordion>

  <Accordion title="Prefer Percent-Based over Cost-Based when the split is fixed" icon="circle-check">
    Cost-Based and Business Metrics-Based allocations aggregate a driver and split each matched row across every target it touches. If the split between teams is a fixed percentage, a Percent-Based allocation is still an allocation but fans out across only those few percentages, so it produces the same result with much less work.
  </Accordion>

  <Accordion title="Consolidate small virtual tags" icon="circle-check">
    Using 11 narrow virtual tags that each cover a single team takes longer to process and is harder to reason about than one `team` virtual tag with 11 values. Consolidate where possible and keep headroom under the default limits of [50 virtual tags per account and 100 values per tag](/tagging#virtual-tags-limit).
  </Accordion>
</AccordionGroup>

## Anti-Patterns to Avoid

<AccordionGroup>
  <Accordion title="All Providers on every tag" icon="circle-xmark">
    Convenient by default, slow at scale. Scope each tag to the providers whose data its filters actually match.
  </Accordion>

  <Accordion title="Backfill set to the earliest period &#x22;just in case&#x22;" icon="circle-xmark">
    A forward-looking tag with a longer backfill reprocesses three years of data on every save for no reporting benefit. Set the backfill to the earliest month you actually report on for that tag.
  </Accordion>

  <Accordion title="Four or more levels of nesting" icon="circle-xmark">
    Each level is sequential. Flatten where possible. If a deep chain feels necessary, look for a level whose mapping is stable enough to inline into its parent.
  </Accordion>

  <Accordion title="Iterating on a tag with many saves" icon="circle-xmark">
    Every save kicks off reprocessing on every relevant import. Stage all your changes (values, filters, ordering, backfill) before saving, especially for tags with wide backfill windows.
  </Accordion>

  <Accordion title="Using flexible match where Tag Key Collapsing would work" icon="circle-xmark">
    `flexible match` is the slowest operator. If the variation is in the *key* rather than the value, use Tag Key Collapsing.
  </Accordion>

  <Accordion title="Many independent allocated tags on the same provider" icon="circle-xmark">
    Each allocated tag chain redistributes costs into a separate dataset, and unrelated allocated tags cannot be combined on the same Cost Report. Keep allocated tags inside the same chain when you need them to compose, or use separate reports.
  </Accordion>
</AccordionGroup>

## Frequently Asked Questions

<AccordionGroup>
  <Accordion title="Why did my tag take hours to process?">
    Processing time is roughly proportional to `billing periods in backfill` × `integrations whose data the tag could match` × `values + collapsed keys` × `nested levels`, and for allocated tags it scales further with the number of targets each cost row splits into. A `team` tag with 30 values, a 12-month backfill, and All Providers across 8 integrations does an order of magnitude more work than the same tag scoped to AWS only with a 6-month backfill. Most multi-hour cases are explained by a combination of wide backfill and All Providers scoping—or, for allocations, a split across many targets that expands costs into far more line items.
  </Accordion>

  <Accordion title="Does extending the backfill on a parent tag extend it on the tags it references?">
    No. Backfill is per virtual tag. If `business-unit` references `application` and you extend `business-unit`'s backfill to an earlier period, `application` keeps its existing backfill. To align history across a nested chain, extend the backfill on each referenced tag first, wait for processing to complete, then extend the parent.
  </Accordion>

  <Accordion title="Does editing a referenced virtual tag reprocess its dependents?">
    Yes. When you save a tag that other tags reference, Vantage reprocesses the dependent tags as well, in dependency order. This is why nested chains add latency, as edits to a foundational tag ripple through every level above it. More broadly, reprocessing happens per import: when an import is reprocessed, every virtual tag that applies to it reruns, not just the tag you edited. That's another reason to make all your intended edits to a tag before saving, rather than saving after each tweak.
  </Accordion>

  <Accordion title="Does All Providers take longer than per-provider filter sets?">
    Yes for processing, and yes at read time when more than one provider is in scope. For tags whose logic genuinely applies to every provider (and every provider you might add), All Providers is the right choice. For tags whose rules will only ever match one or two providers, scoping per provider is faster and stays accurate as you add new providers that should not be in scope.
  </Accordion>

  <Accordion title="Does the order of values inside a tag affect performance?">
    Order does not change the total amount of matching work, but it does control which rule wins on overlap. Put more specific or higher-priority rules above broader ones so overlapping rules apply the intended value. This is consistent with [Virtual Tag Order](/tagging#virtual-tag-order).
  </Accordion>

  <Accordion title="Can I delete unused values to speed up processing?">
    Yes, modestly. Each removed value is one fewer value Vantage has to match each time the tag runs. The bigger wins come from reducing backfill width, tightening provider scope, and flattening hierarchy. Delete-and-consolidate is worth doing alongside those, not instead of them.
  </Accordion>

  <Accordion title="What about Cost Report performance? Does my virtual tag setup affect that?">
    Mostly indirectly. By the time a Cost Report runs, virtual tag values are already written into your cost data, so report runtime depends largely on the report's filter operators and provider scope. Equality filters on a single provider read faster than `flexible match` filters across All Providers. The exception is allocations: because they split costs into many more rows, a report that groups or filters by an allocated dimension reads more data and can run slower than the same report without that grouping.
  </Accordion>
</AccordionGroup>
