All insights

Platform

Custom Metadata Types vs Custom Settings: which config belongs where

You promote a feature-flag change from sandbox to production and every flag comes up off, because the values never deployed. That one behaviour decides most of this choice. Here is what each tool is, the Apex that reads them without spending a query, the write gotcha that keeps Custom Settings alive, and how to pick.

Custom Metadata Types vs Custom Settings: which config belongs where, article illustration

You promote a feature-flag change from sandbox to production, and every flag comes up off. The fields deployed, the object deployed, and the values did not. Someone now re-enters them by hand in production, from memory, on a Friday.

That one behaviour decides most of the Custom Metadata Types versus Custom Settings debate. Custom metadata records are metadata, so they travel with your deployment. Custom setting values are data, so they do not.

My rule: put configuration in Custom Metadata Types by default, so it is version-controlled and deploys with the app. Keep Custom Settings for the two things metadata can’t do well, per-user or per-profile overrides, and config that Apex has to write at runtime. Everything transactional belongs in a plain custom object instead.

The difference that decides most cases

A Custom Metadata Type record is part of your metadata. It moves through change sets, packages, and the Metadata API, the same way a field or a flow does, so the values you set in a sandbox arrive in production with the deployment. Salesforce leans on this so hard that the Setup screen for creating a list custom setting now suggests you use a custom metadata type instead.

A Custom Setting is different in one place that matters. The definition, the object and its fields, deploys like any metadata. The records inside it do not. You recreate or load those values in every org, by hand or by a script, which is the Friday-afternoon story above.

For configuration that engineering owns and should review in source control, that settles it. Mappings, feature flags, integration endpoints, business rules, all of it belongs where it deploys.

Reading them in Apex without spending a query

Both tools read from an application cache, so neither has to cost you a SOQL query, and both matter when you are watching governor limits.

A hierarchy custom setting resolves per running user. getInstance() returns the merged value, with User settings overriding Profile settings overriding the org default. getOrgDefaults() gives you the org-level values directly.

// Hierarchy custom setting: merged for the running user (User > Profile > Org)
Payment_Settings__c s = Payment_Settings__c.getInstance();
Boolean bypass = s.Bypass_Validation__c;

Payment_Settings__c orgDefaults = Payment_Settings__c.getOrgDefaults();

Custom Metadata Types got matching static methods so a read need not touch SOQL either. getAll() returns every record keyed by developer name, and getInstance(developerName) returns one.

// Custom metadata: cached reads, no SOQL consumed
Country_Mapping__mdt us = Country_Mapping__mdt.getInstance('United_States');
String iso = us.ISO_Code__c;

Map<String, Country_Mapping__mdt> all = Country_Mapping__mdt.getAll();

You can still query a custom metadata type with SOQL when you need a WHERE filter or a long-text-area field the static methods don’t return. Be aware of the trade: a SOQL query against a custom metadata type counts as one of your queries, though it does not count against the row limit. When a cached getAll or getInstance will do, prefer it.

The write gotcha that keeps Custom Settings alive

Here is the asymmetry that catches people. You cannot create or update a custom metadata record with ordinary Apex DML. insert myRecord__mdt does not compile the way you expect, because the record is metadata, not data.

To change a custom metadata record from Apex you build a Metadata.CustomMetadata object and call Metadata.Operations.enqueueDeployment, which runs asynchronously, needs a callback, can create and update but not delete, and is capped on how many you can enqueue at once. Fine for an admin-driven setup action; wrong for anything on a synchronous user path.

A custom setting, by contrast, takes a plain upsert at runtime. So when Apex has to write configuration or state as part of a transaction, a custom setting is the tool and custom metadata is not. That, plus the hierarchy, is why custom settings are not going anywhere and are not deprecated.

What each one does that the other can’t

Custom Metadata Types carry capabilities custom settings never had. They support relationship fields, including a metadata relationship to another type or to an EntityDefinition, and fields that point at an object or a field by API name. They expose field-level manageability, protected or public and subscriber-editable, which is how a managed package controls what an installing org may see or change. And their values are usable directly in formulas, validation rules, flows, and default values through $CustomMetadata.

Custom Settings keep two edges. The hierarchy, which resolves a value per user and per profile with no code, is the big one, and custom metadata has no equivalent. Hierarchy settings are also readable in formulas through $Setup, so an admin can branch a validation rule on a per-profile toggle.

The practical shape of this shows up in a bypass switch. The metadata-driven trigger bypass that disables a handler by config fits custom metadata, because the rule ships with the code. A “skip validation for this one integration user” toggle fits a hierarchy custom setting, because it is inherently per user, and it pairs with the validation-rule bypass pattern an agent write needs.

A quick map

NeedReach for
Config that deploys and is version-controlledCustom Metadata Type
Values packaged and shipped to subscribersCustom Metadata Type
Used in a formula, validation rule, or flowCustom Metadata Type ($CustomMetadata)
Per-user or per-profile overrideHierarchy Custom Setting
Written by Apex at runtimeCustom Setting (plain DML)
Transactional data, volume, reports, sharingCustom Object

How I choose

Start with one test: does the value belong to the app, or to a person. App configuration, the kind you would want to see in a pull request and deploy as a unit, goes in a Custom Metadata Type. It reads without a query, ships in your package, and an admin can wire it into a flow or validation rule without you.

A value that differs by user or profile, or that Apex has to write mid-transaction, goes in a Custom Setting, almost always the hierarchy kind. And the moment the thing is really records, created and edited by users, reported on, shared, or counted in the thousands, it is not configuration at all. It is a custom object, and forcing it into either settings tool is technical debt you will pay down later.

One note on tests, because it bites. Custom metadata records are visible in Apex tests without SeeAllData, since they are metadata. Custom setting data is treated as org data and is not visible by default, so a test that needs a setting has to insert it in setup. Build that into your test data factory rather than discovering it in a red build.

If you take one thing from this: default new configuration to Custom Metadata Types, and when you catch yourself reaching for a custom setting, say out loud why custom metadata won’t do. Usually the answer is “it’s per user” or “Apex writes it.” If it isn’t one of those, you probably want metadata that deploys, not data that gets left behind.

Understanding the basics

What is the difference between Custom Metadata Types and Custom Settings?

Custom Metadata Type records are metadata, so their values deploy through change sets and packages from sandbox to production. Custom Setting records are data: the object and fields deploy, but the values do not, so you recreate them in each org. Custom metadata is the default for deployable configuration; custom settings suit per-user hierarchy values and config Apex writes at runtime.

Can I update Custom Metadata Types in Apex?

Not with ordinary DML. You build a Metadata.CustomMetadata record and call Metadata.Operations.enqueueDeployment, which is asynchronous and needs a callback. Because of that, config a transaction must write synchronously is better kept in a custom setting, which accepts a plain upsert.

Do Custom Settings or Custom Metadata Types count against SOQL limits?

Reading a custom setting with getInstance, getOrgDefaults, or getAll uses the application cache and issues no SOQL. Custom Metadata Types have matching getInstance and getAll methods that also avoid SOQL. A SOQL query against a custom metadata type counts as one query but not against the row limit, so prefer the cached methods when you can.

Are Custom Settings deprecated?

No. Salesforce steers new configuration toward Custom Metadata Types and suggests them over list custom settings in Setup, but custom settings remain fully supported. Hierarchy custom settings in particular do something custom metadata cannot: resolve a value per user and per profile.


Untangling an org where configuration is scattered across settings, metadata, and hardcoded Apex, and half of it doesn’t survive a deployment? Talk to us. Making an org cheap and safe to change again is the work we do.

Keep reading

All insights