---
title: "Custom metadata, custom settings, or a constant"
url: "https://trailassembly.com/custom-metadata-vs-custom-settings/"
topic: "Apex and Development"
published: "2026-09-11T07:24:20+00:00"
updated: "2026-10-06T13:52:45+00:00"
---

# Custom metadata, custom settings, or a constant

**The short version:** custom metadata types deploy with your code and do not count against SOQL limits. Custom settings do not deploy their data, and hierarchy custom settings can vary per user or profile, which metadata cannot. So the rule is: configuration that should travel with a release goes in custom metadata. Per-user overrides go in a hierarchy custom setting. And constants go in neither.

## The property that decides most cases

Custom metadata records are metadata. They move in a deployment, they live in your repository, and the value that was reviewed in the pull request is the value that arrives in production. Custom setting *definitions* deploy. Custom setting *data* does not. Someone re-enters it in every org, by hand, every time.

That single difference settles most arguments. An integration endpoint, a threshold, a mapping table, a feature flag with a release attached: all of those should arrive with the code that reads them, which means custom metadata.

## The limits difference

Custom metadata reads do not count against the SOQL query limit, and the records are cached. You can query one inside a loop and nothing bad happens, which is the only place on the platform where that sentence is true.

```apex
Shipping_Rule__mdt rule = Shipping_Rule__mdt.getInstance('Standard');
Decimal cost = rule.Base_Cost__c;
```

List custom settings are also cached and also free of the query limit. Hierarchy custom settings are cached per user. So neither is expensive to read, and the limits are not the deciding factor — deployability is.

## What only hierarchy custom settings can do

A hierarchy custom setting resolves a value per user, falling back to their profile, falling back to the org default. Nothing in custom metadata does this. If you need “batch size is 200, except 50 for the data team, except 10 for this one integration user”, that is a hierarchy custom setting and there is no argument.

```apex
Integration_Config__c cfg = Integration_Config__c.getInstance();
Integer batchSize = (Integer) cfg.Batch_Size__c;
```

`getInstance()` with no argument does the whole hierarchy resolution for the [running user](https://trailassembly.com/dashboard-running-user/). That is the feature. Using a hierarchy setting and then always calling `getOrgDefaults()` means you wanted custom metadata.

## What belongs in neither

A value that will never change without a code change is a constant. Putting it in custom metadata does not make it configurable, it makes it invisible: the next developer reads the class, sees `getInstance`, and has to go to Setup to learn that the answer is always 30.

Three questions decide it. Will a non-developer ever change this? Does it differ between orgs? Does it differ between users? Three noes means `static final`.

## Where custom metadata bites

- **You cannot write to it in Apex synchronously.** Updates go through the [Metadata API](https://trailassembly.com/apex-metadata-api-from-apex/), asynchronously, and they are a deployment. Anything your code needs to write during a transaction cannot live here.

- **Tests see real records regardless.** Custom metadata is visible in tests without `SeeAllData`, which is convenient and also means a test can pass because of a record someone added in a sandbox. Where the value matters to the [assertion](https://trailassembly.com/apex-assert-exception-message/), inject it rather than reading it.

- **Relationships are limited.** Metadata relationship fields point at other metadata types or entity definitions, not at records.

## Where custom settings bite

- **The data does not deploy.** Every new sandbox starts empty, so a refresh silently removes your configuration and the first failure is a null reference at runtime.

- **List custom settings are effectively deprecated.** Salesforce points new work at custom metadata, and list settings offer nothing metadata does not.

- **No [field-level security](https://trailassembly.com/apex-enforcing-crud-and-fls/) worth the name.** Custom settings are protected by their own visibility flag, not by the ordinary model.

## Migrating from custom settings

Do it a setting at a time, not all at once. Create the metadata type, populate it, change the reading code to a method that returns the value, and delete the custom setting once nothing references it. The read-through method is the important step: it gives you one place to change, and it lets both sources coexist while you migrate.

## Frequently asked

**Can a flow read custom metadata?** Yes, with a [Get Records](https://trailassembly.com/flow-get-records-behaviour/) element, and it is free of limits there too.

**Do custom metadata records count against data storage?** No. They are metadata, and they count against metadata limits instead.

**Can I use custom metadata for feature flags?** Yes, and it is the best use of it, because turning the flag on is a deployment with a review and a history. If you need to flip it in production without a deploy, you need a custom setting and you should accept the trade.

---

Source: https://trailassembly.com/custom-metadata-vs-custom-settings/
Trail Assembly — independent Salesforce write-ups.
