ESC

AI-powered search across all blog posts and tools

Architecture · December 12, 2025

Custom Metadata Types vs Custom Settings - Complete Comparison

A deep-dive comparison of Custom Metadata Types and Custom Settings to help you make the right architectural choice every time.

☕ 9 min read 📅 December 12, 2025
  • Custom Metadata Types are deployable and versionable — they travel with your package or changeset
  • Custom Settings (Hierarchy) are ideal for per-profile or per-user configuration overrides
  • Use Custom Metadata for anything that should be the same across all environments; use Custom Settings for environment-specific values

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

Custom Metadata Types vs Custom Settings — Feature Comparison
Custom Metadata TypesCustom SettingsFeatureDeployable via Changeset/SFDXYES (metadata)NO (data)Packagable (ISV)YESNOHierarchy (Org/Profile/User)NOYES (Hierarchy type)Available in SOQLYESYes (also getInstance() for cached access)Counts against SOQL limitsYES (cached after 1st)NO via getInstance(); YES via SOQLEditable in Setup UIYESYESDML in Apex (insert/update)NO (use Metadata API)YESRow limitUp to 10MB per object10MB (Hierarchy), unlimited (List)Supports relationshipsYES (to other CMTs)NOProtected (Managed packages)YESNO

Decision Flowchart: Which One Should I Use?

Configuration Type Decision Flowchart
Config Needed?Must it deploy with code?YESNeed per-user/profile override?NOCMTYESCMT for defaults + Hierarchy Custom Setting for overridesNONeed hierarchy(per-user override)?YESHierarchy Custom SettingNO

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.

The Problem

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.

The Solution

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);
ℹ️ Info

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

🚨 Never Store Secrets in Custom Settings

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.

💡 Pro Tip

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

Why did the integration break on go-live when the team used Custom Settings for configuration?
When should you use the hybrid pattern (CMT + Custom Setting)?

How did this article make you feel?

Comments

Salesforce Tip

🎉

You finished this article!

What to read next

Contents