I’ve seen this architectural decision made incorrectly more times than I can count. A developer builds a beautifully dynamic system using Custom Settings — config values, mapping tables, feature flags — and then deployment day arrives and none of that data travels to production because Custom Settings data isn’t included in change sets by default.
Or the opposite: someone models a hierarchy config in Custom Metadata and then can’t understand why they can’t override it per user. The two tools solve different problems, and choosing the wrong one creates real pain downstream.
Let me walk through everything you need to know to make the right call.
The Core Difference in One Sentence
Custom Metadata Types are metadata — they deploy with your code, version with your package, and are the same in every org unless explicitly changed. Custom Settings are data — they live in the org, don’t deploy automatically, and support hierarchy overrides (by profile or user).
Side-by-Side Comparison
Decision Flowchart: Which One Should I Use?
Real-World Use Cases
Use Custom Metadata Types For…
Feature Flags
Feature flags and toggles. Deploy a Feature_Flag__mdt object with a Is_Enabled__c checkbox. Toggle features per environment by deploying the record, not running scripts.
Feature_Flag__mdt flag = Feature_Flag__mdt.getInstance('New_Checkout_Flow');
if (flag != null && flag.Is_Enabled__c) {
// use new flow
}Integration Mapping
Integration endpoint mapping. Store named credential paths, field mappings, or routing logic that should be identical in QA and production.
List<Integration_Mapping__mdt> mappings = [
SELECT Source_Field__c, Target_Field__c
FROM Integration_Mapping__mdt
WHERE Object_API_Name__c = 'Account'
];Validation Bypass
Validation rule bypass lists. Store a list of record types or profiles exempt from a validation in metadata instead of hardcoding in the rule formula.
Use Custom Settings (Hierarchy) For…
Environment-Specific Config
Environment-specific API keys. Your UAT environment hits a sandbox API endpoint; production hits the live endpoint.
My_App_Settings__c settings = My_App_Settings__c.getOrgDefaults();
String apiEndpoint = settings.API_Endpoint__c;Per-User Feature Toggles
Feature toggles that need per-user control. Enable a beta feature for specific users before a full rollout:
My_App_Settings__c userSettings = My_App_Settings__c.getInstance(UserInfo.getUserId());
Boolean betaEnabled = userSettings.Beta_Features_Enabled__c;Admin-Adjustable Thresholds
Threshold values that admins adjust without a deployment. Rate limits, batch sizes, email quotas — things that ops teams tweak in production without a release cycle.
Scenario: Your team used Hierarchy Custom Settings to store integration endpoint URLs, batch size thresholds, and feature flags across a complex implementation. Everything worked great in sandbox. On deployment day, none of the configuration values exist in production — the Custom Settings records weren’t included in the change set — and the integration is broken at go-live.
Migrate all deploy-time configuration (integration endpoints, feature flags, field mappings) to Custom Metadata Types. CMT records are metadata — they travel with your deployment automatically. Reserve Custom Settings only for values that are intentionally different per environment or need per-user hierarchy overrides, such as user-specific UI preferences or environment-specific API credentials.
Code Examples: Accessing Both
Custom Metadata Type
SOQL Access
// Query all records
List<Country_Tax_Rate__mdt> rates = [
SELECT Country_Code__c, Tax_Rate__c
FROM Country_Tax_Rate__mdt
WHERE Is_Active__c = true
];
// Direct instance access (developer name)
Country_Tax_Rate__mdt usRate = Country_Tax_Rate__mdt.getInstance('US');
Decimal taxRate = usRate.Tax_Rate__c;The getInstance() static method is generated automatically for every CMT. No SOQL query needed — and it’s cached by the platform.
DML in Apex
// Create a new CMT record programmatically via Metadata API
Metadata.CustomMetadata cmRecord = new Metadata.CustomMetadata();
cmRecord.fullName = 'Country_Tax_Rate__mdt.CA';
cmRecord.label = 'CA';
Metadata.CustomMetadataValue countryField = new Metadata.CustomMetadataValue();
countryField.field = 'Country_Code__c';
countryField.value = 'CA';
cmRecord.values.add(countryField);
Metadata.CustomMetadataValue rateField = new Metadata.CustomMetadataValue();
rateField.field = 'Tax_Rate__c';
rateField.value = 5.0;
cmRecord.values.add(rateField);
Metadata.CustomMetadataValue activeField = new Metadata.CustomMetadataValue();
activeField.field = 'Is_Active__c';
activeField.value = true;
cmRecord.values.add(activeField);
Metadata.DeployContainer container = new Metadata.DeployContainer();
container.addMetadata(cmRecord);
Metadata.Operations.enqueueDeployment(container, null);Custom Metadata Types cannot be created or updated using standard DML (insert/update). You must use the Metadata.DeployContainer and Metadata.Operations.enqueueDeployment() approach shown above. The deployment is asynchronous — pass a Metadata.DeployCallback implementation as the second argument to track the result.
Custom Settings (Hierarchy)
Hierarchy Access
// Get the most specific value in the hierarchy (User > Profile > Org)
My_Settings__c settings = My_Settings__c.getInstance();
// Get org-level defaults only
My_Settings__c orgDefaults = My_Settings__c.getOrgDefaults();
// Get for a specific user
My_Settings__c userSettings = My_Settings__c.getInstance(someUserId);
// Get for a specific profile
My_Settings__c profileSettings = My_Settings__c.getInstance(someProfileId);The getInstance() without arguments walks the hierarchy and returns the most specific match. This is the magic of Hierarchy Custom Settings.
DML
// Upsert org-level settings
My_Settings__c orgConfig = My_Settings__c.getOrgDefaults();
orgConfig.Max_Records__c = 500;
orgConfig.Timeout_Seconds__c = 30;
upsert orgConfig;
// Set per-user override
My_Settings__c userOverride = new My_Settings__c(
SetupOwnerId = someUserId,
Max_Records__c = 100 // this user gets a lower limit
);
upsert userOverride;Common Mistakes
Neither Custom Settings nor CMTs are encrypted by default. Use Named Credentials for API keys, or Shield Platform Encryption for sensitive field-level data.
Using Custom Settings for deploy-time config. If your integration URL changes between environments and you rely on Custom Settings, you’ll forget to update production. Use CMT + a deployment step instead.
Querying CMTs in a loop. Even though CMTs are cached after the first query, you still burn a SOQL call. Query once, store in a Map keyed by DeveloperName:
Map<String, Tax_Rate__mdt> rateMap = new Map<String, Tax_Rate__mdt>();
for (Tax_Rate__mdt r : [SELECT DeveloperName, Rate__c FROM Tax_Rate__mdt]) {
rateMap.put(r.DeveloperName, r);
}Forgetting that Hierarchy Custom Settings aren’t packagable. If you’re building a managed package, Hierarchy Custom Settings records won’t install in the subscriber’s org. Use CMT for any configuration that must ship with the package.
For large-scale implementations, consider a hybrid pattern: use a Custom Metadata Type to store your canonical configuration defaults, and a Hierarchy Custom Setting with a single Override_JSON__c long text field for environment or user-specific overrides. Your Apex reads the CMT first, then checks for a Custom Setting override and merges. This gives you the deployability of CMT with the flexibility of Custom Settings hierarchy — without duplicating your entire data model.
The right choice usually comes down to one question: does this value change between environments, or is it the same everywhere? If it changes per environment, lean toward Custom Settings. If it travels with your code, use Custom Metadata.
What configuration pattern have you found most useful in large multi-org implementations? Are you still reaching for Custom Settings in 2026, or have CMTs fully taken over for you? I’m particularly curious about how teams handle the hybrid scenario where some values must deploy and others must remain environment-specific.
Test Your Knowledge
How did this article make you feel?
Comments
Salesforce Tip