Here is a question I like to ask developers who are a few months into Salesforce: your app has a threshold in it somewhere — say, the number of days before a case counts as stale. Where does that number live?
If the answer is “in an Apex class” — Apex being Salesforce’s programming language — we have something to talk about. If the answer is “in a Custom Setting” or “in a Custom Metadata record”, my next question is: why that one and not the other?
That second question stumps more people than the first. Salesforce gives us two dedicated homes for configuration — Custom Metadata Types and Custom Settings — and at first glance they look almost identical. Both are little tables of settings that live outside your code. Both keep you from hardcoding values. But they behave very differently the moment your app has to move between orgs — the individual Salesforce environments your company runs — and choosing the wrong one is one of the quietest ways a project accumulates pain. Let me walk through the difference the way I would explain it to a class.
One sentence that separates them
Both tools hold configuration outside code. The deep difference fits in one sentence:
Custom Metadata Type records are metadata. Custom Settings records are data.
Let me unpack those two words, because everything else follows from them. In Salesforce, metadata is the description of your org — your objects, your fields, your Apex classes, your page layouts. Metadata is what deploys: when you move a feature from a sandbox (a practice copy of your org) to production, you are moving metadata. Data, on the other hand, is the records living inside those structures — the actual accounts, cases, and rows. Data does not travel with a deployment.
So when I say Custom Metadata records are metadata, I mean the records themselves — not just the table definition, but every row you create — deploy between orgs and can ship inside a package. And when I say Custom Settings records are data, I mean that after you deploy the Custom Setting’s definition, the org receives an empty table. Someone has to create the records in each org, every time.
That one distinction decides almost every real-world choice between them.
Custom Metadata: configuration that travels with your app
Because Custom Metadata records deploy and package like code, they behave like part of the application. Build a validation threshold as a Custom Metadata record in your sandbox, deploy it, and production receives both the structure and the value.
Reading them in Apex is pleasant too. You can fetch records with getAll() or getInstance() without consuming any of your SOQL queries — SOQL being Salesforce’s query language, which is rationed by governor limits, the per-transaction budgets Salesforce enforces so no single piece of code can hog the shared platform. Even if you do write a SOQL query against a Custom Metadata Type, it does not count against the per-transaction SOQL limit.
// One record by developer name — no SOQL consumed
Alert_Threshold__mdt row = Alert_Threshold__mdt.getInstance('Case_Age_Warning');
// Or every record at once
Map<String, Alert_Threshold__mdt> rows = Alert_Threshold__mdt.getAll();
There is one more feature worth knowing if you ever build a managed package — the installable, upgradeable app format used on the AppExchange. Custom Metadata marked as protected inside a managed package is invisible to subscribers, the customers who install it. That makes it the right home for internal knobs: values your app needs, tuned by you, that installing admins should neither see nor edit.
Custom Settings: configuration that stays home
If Custom Metadata is so convenient, why do Custom Settings still exist? Because they can do two things Custom Metadata cannot.
The first is the unique strength of the hierarchy type of Custom Setting: values can be set at the org level, then overridden per profile (the permission template a user belongs to), then overridden again per individual user. If you need a feature switched on for one team while the rest of the org waits, or a toggle that a specific user can have flipped just for them, hierarchy Custom Settings are built for exactly that.
The second is secrecy within the org. A protected Custom Setting keeps its values out of sight, which makes it a reasonable place for org-local sensitive values — an integration credential, for example — instead of leaving them somewhere visible.
But remember the one-sentence rule: these records are data. They do not travel with a deployment. Every sandbox refresh — the operation that wipes a sandbox and rebuilds it as a fresh copy of production — and every brand-new org starts with the table empty. Someone must re-create the records by hand or by script. This is exactly how configuration silently goes missing: the code deploys fine, the tests were passing last week, and then a feature quietly behaves differently because a setting record that everyone forgot about no longer exists.
Custom Metadata records deploy with your app. Custom Settings records stay behind — and anything that stays behind will, one day, be forgotten.
The rule of thumb I teach
When a student asks me which to use, I offer this:
If the value is part of how the app works — thresholds, mappings, feature switches, business rules — put it in Custom Metadata, so it deploys and versions like the code it belongs to. If the value varies per user or profile, or it is an org-local secret, put it in a Custom Setting.
Notice the shape of the rule. It is about the nature of the value, not about which tool you happen to know. Application logic wants to travel with the application. User preferences and local secrets want to stay local.
I lean on both halves of this rule in my own builds. In my monitoring work, every tunable threshold lives in Custom Metadata — which means calibrating those values for a particular org never requires a code change or a deployment. And in my Urla Shoes build, the integration secrets live in protected Custom Settings, so nothing sensitive ever sits in source control.
The anti-pattern both of them exist to prevent
There is a third place configuration ends up, and it is the worst one: hardcoded constants sprinkled through Apex. A discount limit here, a retry count there, an email address in a class nobody has opened in a year.
The cost arrives slowly. Every tuning request — “can we change that threshold to be a little stricter?” — becomes a code change, a review, and a deployment. And because each org’s copy of the code can lag behind the others, every org drifts. The number you see in the class is no longer the number you can trust everywhere.
A constant in Apex is a configuration decision you have made permanent by accident.
One honest caveat before you go apply any of this: platform limits and edition details can change over time, and packaging behaviour depends on your setup — so verify the current behaviour in your org before you lean on it in a design.
Your next step
Open your org’s Apex classes and search for a literal value you recognize as a business decision — a threshold, a limit, a mapping. Ask the one-sentence question about it: is this part of how the app works, or is it something that varies by user or belongs only to this org?
If it is part of how the app works, move it into a Custom Metadata Type and read it with getInstance(). If it is per-user or a local secret, give it a Custom Setting. Either way, you will have taken one decision out of the code and put it where configuration should live — and the next sandbox refresh, the next deployment, and the next tuning request will all be a little calmer for it.