Introduction: The Configuration Conundrum
ESalesforce Custom Metadata vs Custom Settings is not just a technical comparison—it’s a decision that directly impacts how scalable, maintainable, and deployment-ready your entire Salesforce architecture will be. Most developers underestimate this choice early on, treating configuration storage as a minor detail. That’s a mistake. The wrong approach here doesn’t fail immediately—it creates slow, painful problems over time.

At some point, every Salesforce architect faces the same challenge: where should configuration data live? Whether it’s feature toggles, business rules, integration endpoints, or environment-specific values, this data controls how your application behaves. Choosing between Custom Settings, Custom Metadata Types, and Custom Labels isn’t about preference—it’s about understanding trade-offs at a deeper level.
Custom Settings were once the default solution. They offer fast access and simplicity, but they lack modern deployment flexibility and can create issues in multi-environment setups. Custom Labels, while useful, are often misused by developers trying to avoid proper design—they are meant strictly for text and translation, not for application logic or structured configuration.
Custom Metadata Types represent Salesforce’s evolution in configuration management. They are deployable, package-friendly, and designed for long-term scalability. But even they are not perfect—limitations like restricted runtime updates can become blockers if you don’t plan correctly.
This comparison goes beyond surface-level definitions. We’ll break down how each option behaves in real-world scenarios, how they interact with governor limits, and how they impact CI/CD pipelines. Because if you’re still choosing based on convenience instead of architecture, you’re not building a system—you’re building future problems.
Understanding Custom Settings
Technical Architecture
Custom Settings are essentially custom objects with special characteristics that allow application-wide or profile/user-specific data storage. Salesforce stores Custom Settings data in the application cache, making retrieval faster than standard SOQL queries against custom objects.
Custom Settings come in two flavors:
1. List Custom Settings: Store reusable, static data accessible across your organization regardless of user or profile.
2. Hierarchy Custom Settings: Provide organization-wide defaults with the ability to override at profile or user level, following a hierarchy: Organization → Profile → User.
Technical Implementation
Here’s a practical example implementing an API integration configuration using Custom Settings:
apex// Custom Setting: API_Configuration__c (Hierarchy)
// Fields: Endpoint__c, Timeout__c, Retry_Count__c, API_Key__c
public class APIConfigurationManager {
// Retrieves hierarchical custom setting with caching
public static API_Configuration__c getConfig() {
API_Configuration__c config = API_Configuration__c.getInstance();
if (config == null || config.Endpoint__c == null) {
throw new ConfigurationException('API Configuration not found for current context');
}
return config;
}
// Example usage in integration class
public static HttpResponse callExternalAPI(String payload) {
API_Configuration__c config = getConfig();
HttpRequest req = new HttpRequest();
req.setEndpoint(config.Endpoint__c);
req.setTimeout(Integer.valueOf(config.Timeout__c));
req.setMethod('POST');
req.setBody(payload);
req.setHeader('Authorization', 'Bearer ' + config.API_Key__c);
Http http = new Http();
HttpResponse response;
Integer retryCount = 0;
do {
try {
response = http.send(req);
if (response.getStatusCode() == 200) {
break;
}
} catch (Exception e) {
retryCount++;
if (retryCount >= config.Retry_Count__c) {
throw e;
}
}
} while (retryCount < config.Retry_Count__c);
return response;
}
}
Governor Limits and Performance
Custom Settings don’t count against SOQL query limits when using getInstance() or getValues() methods, as they’re retrieved from the application cache. However:
- Maximum 300 Custom Settings per org
- List Custom Settings limited to 10 MB per org
- Each Custom Settings record counts against the 10 MB limit
- Hierarchy Custom Settings are subject to standard data storage limits
Deployment Characteristics
Custom Settings data requires manual configuration across environments. The structure (fields) deploys via metadata, but the data itself doesn’t. This creates friction in CI/CD pipelines:
XML<!-- CustomSettings metadata deploys, but not the data -->
<customObject xmlns="http://soap.sforce.com/2006/04/metadata">
<customSettingsType>Hierarchy</customSettingsType>
<enableFeeds>false</enableFeeds>
<label>API Configuration</label>
<!-- Fields deploy here -->
</customObject>
You’ll need to write post-deployment scripts or use tools like the Salesforce CLI data import/export to manage Custom Settings data across sandboxes and production.
Understanding Custom Metadata Types
Technical Architecture
Custom Metadata Types (CMDTs) represent Salesforce’s modern approach to configuration management. Unlike Custom Settings, CMDTs are true metadata—their records deploy alongside your code through change sets, packages, or CI/CD pipelines.
Architecturally, CMDTs provide:
- Full metadata API support
- Declarative relationships to other metadata (Entity Definitions, Field Definitions)
- Support for record types, validation rules, and protected custom metadata within managed packages
- Transactional safety in deployments
Technical Implementation
Here’s a sophisticated example using Custom Metadata Types for a multi-channel notification routing system:
apex// Custom Metadata Type: Notification_Channel__mdt
// Fields: Channel_Name__c, Handler_Class__c, Priority__c, Active__c,
// Retry_Logic__c, Max_Retries__c
public class NotificationRouter {
private static Map<String, Notification_Channel__mdt> channelConfigCache;
// Load all active channels with efficient caching
static {
channelConfigCache = new Map<String, Notification_Channel__mdt>();
for (Notification_Channel__mdt channel : [
SELECT Channel_Name__c, Handler_Class__c, Priority__c,
Retry_Logic__c, Max_Retries__c, Active__c
FROM Notification_Channel__mdt
WHERE Active__c = true
ORDER BY Priority__c ASC
]) {
channelConfigCache.put(channel.Channel_Name__c, channel);
}
}
public static void sendNotification(String channelName, String message, Map<String, Object> params) {
if (!channelConfigCache.containsKey(channelName)) {
throw new ChannelNotFoundException('Channel not configured: ' + channelName);
}
Notification_Channel__mdt config = channelConfigCache.get(channelName);
// Dynamic handler instantiation based on metadata
Type handlerType = Type.forName(config.Handler_Class__c);
if (handlerType == null) {
throw new ConfigurationException('Handler class not found: ' + config.Handler_Class__c);
}
INotificationHandler handler = (INotificationHandler)handlerType.newInstance();
// Execute with retry logic from metadata
executeWithRetry(handler, message, params, config);
}
private static void executeWithRetry(INotificationHandler handler, String message,
Map<String, Object> params,
Notification_Channel__mdt config) {
Integer attempts = 0;
Boolean success = false;
while (attempts < config.Max_Retries__c && !success) {
try {
handler.send(message, params);
success = true;
} catch (Exception e) {
attempts++;
if (attempts >= config.Max_Retries__c) {
// Log final failure
System.debug(LoggingLevel.ERROR,
'Notification failed after ' + attempts + ' attempts: ' + e.getMessage());
throw e;
}
// Implement backoff strategy from metadata
if (config.Retry_Logic__c == 'Exponential') {
// Wait logic here (in async context)
}
}
}
}
// Interface for handler implementations
public interface INotificationHandler {
void send(String message, Map<String, Object> params);
}
}
// Example handler implementation
public class EmailNotificationHandler implements NotificationRouter.INotificationHandler {
public void send(String message, Map<String, Object> params) {
Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
email.setToAddresses(new List<String>{(String)params.get('recipient')});
email.setSubject((String)params.get('subject'));
email.setPlainTextBody(message);
Messaging.sendEmail(new List<Messaging.SingleEmailMessage>{email});
}
}
Metadata Relationships and Advanced Features
Custom Metadata Types support relationships to standard and custom objects, as well as other metadata types:
apex// Custom Metadata Type: Field_Validation_Rule__mdt
// Fields: Object_API_Name__c (EntityDefinition), Field_API_Name__c (FieldDefinition),
// Validation_Type__c, Error_Message__c
public class DynamicFieldValidator {
public static void validateRecord(SObject record) {
String objectName = record.getSObjectType().getDescribe().getName();
List<Field_Validation_Rule__mdt> rules = [
SELECT Field_API_Name__c, Validation_Type__c, Error_Message__c,
Min_Value__c, Max_Value__c, Regex_Pattern__c
FROM Field_Validation_Rule__mdt
WHERE Object_API_Name__c = :objectName
AND Active__c = true
];
for (Field_Validation_Rule__mdt rule : rules) {
Object fieldValue = record.get(rule.Field_API_Name__c);
switch on rule.Validation_Type__c {
when 'Range' {
validateRange(fieldValue, rule);
}
when 'Regex' {
validatePattern(fieldValue, rule);
}
when 'Required' {
validateRequired(fieldValue, rule);
}
}
}
}
private static void validateRange(Object value, Field_Validation_Rule__mdt rule) {
if (value == null) return;
Decimal numValue = Decimal.valueOf(String.valueOf(value));
if (numValue < rule.Min_Value__c || numValue > rule.Max_Value__c) {
throw new ValidationException(rule.Error_Message__c);
}
}
private static void validatePattern(Object value, Field_Validation_Rule__mdt rule) {
if (value == null) return;
String stringValue = String.valueOf(value);
Pattern p = Pattern.compile(rule.Regex_Pattern__c);
if (!p.matcher(stringValue).matches()) {
throw new ValidationException(rule.Error_Message__c);
}
}
private static void validateRequired(Object value, Field_Validation_Rule__mdt rule) {
if (value == null || String.valueOf(value).trim() == '') {
throw new ValidationException(rule.Error_Message__c);
}
}
public class ValidationException extends Exception {}
}
Governor Limits and Performance
Custom Metadata Types are queried using SOQL, which means:

- They count against the 100 SOQL query limit per transaction
- Each query returns up to 50,000 records
- Maximum 100 Custom Metadata Types per org
- Maximum 300 fields per Custom Metadata Type
- Records are cached automatically by Salesforce’s platform cache
Critical performance consideration: When querying Custom Metadata Types, leverage static initialization blocks for application-wide configuration to minimize SOQL queries:
apexpublic class ConfigurationCache {
public static final Map<String, Integration_Config__mdt> INTEGRATION_CONFIGS {
get {
if (INTEGRATION_CONFIGS == null) {
INTEGRATION_CONFIGS = new Map<String, Integration_Config__mdt>();
for (Integration_Config__mdt config : Integration_Config__mdt.getAll().values()) {
INTEGRATION_CONFIGS.put(config.DeveloperName, config);
}
}
return INTEGRATION_CONFIGS;
}
private set;
}
}
Deployment Characteristics
Custom Metadata Types deploy as true metadata, making them ideal for CI/CD pipelines:
XML<!-- Both structure and data deploy together -->
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata">
<label>Production API Config</label>
<protected>false</protected>
<values>
<field>Endpoint__c</field>
<value>https://api.production.example.com</value>
</values>
<values>
<field>Timeout__c</field>
<value>30000</value>
</values>
</CustomMetadata>
Understanding Custom Labels
Technical Architecture
Custom Labels are text values stored as metadata specifically designed for:
- Multi-language support (internationalization/i18n)
- Externalization of user-facing text
- Centralized message management
- Dynamic text injection in Visualforce, Lightning, and Apex
Custom Labels support translation workbench integration and can reference merge fields.
Technical Implementation
apex// Custom Label Examples: API_Error_Message, Record_Save_Success, Validation_Error_Pattern
public class UserFacingMessages {
// Using custom labels for error handling
public static void processRecord(Account acc) {
try {
validateAccount(acc);
upsert acc;
// Success message with merge field
String successMsg = String.format(
System.Label.Record_Save_Success,
new List<String>{acc.Name, String.valueOf(acc.Id)}
);
ApexPages.addMessage(new ApexPages.Message(
ApexPages.Severity.CONFIRM,
successMsg
));
} catch (DmlException e) {
// Error message from custom label
ApexPages.addMessage(new ApexPages.Message(
ApexPages.Severity.ERROR,
System.Label.API_Error_Message + ': ' + e.getMessage()
));
}
}
private static void validateAccount(Account acc) {
// Using custom label for validation pattern
Pattern emailPattern = Pattern.compile(System.Label.Email_Validation_Pattern);
if (!emailPattern.matcher(acc.PersonEmail).matches()) {
throw new ValidationException(
String.format(System.Label.Validation_Error_Pattern,
new List<String>{'Email', acc.PersonEmail})
);
}
}
// Lightning Component usage
@AuraEnabled
public static String getLocalizedMessage(String labelName) {
// Dynamic label access (use cautiously - prefer direct reference)
return System.Label.get('', labelName, UserInfo.getLanguage());
}
}
Governor Limits and Characteristics
- Maximum 5,000 Custom Labels per org
- Maximum 1,000 characters per label
- Labels are cached and don’t count against SOQL limits
- Support up to 127 languages via Translation Workbench
- Labels deploy as metadata but are read-only at runtime
When NOT to Use Custom Labels
Common anti-pattern: Using Custom Labels for application logic configuration:
apex// ❌ ANTI-PATTERN: Don't do this
public static Integer getRetryCount() {
return Integer.valueOf(System.Label.API_Retry_Count); // Wrong!
}
// ✅ CORRECT: Use Custom Metadata or Custom Settings
public static Integer getRetryCount() {
return Integer.valueOf(API_Config__mdt.getInstance('Default').Retry_Count__c);
}
Comprehensive Comparison Table
| Feature | Custom Settings | Custom Metadata Types | Custom Labels |
|---|---|---|---|
| Primary Use Case | User/profile-specific config | Application config & business rules | UI text & internationalization |
| Deploys as Metadata | Structure only | Structure + data | Yes (text content) |
| SOQL Required | No (cached via getInstance()) | Yes (but cached by platform) | No (accessed via System.Label) |
| Counts Against SOQL Limits | No | Yes | No |
| Supports Hierarchy | Yes (Hierarchy type) | No | No |
| Package Support | Limited | Full (protected metadata) | Full |
| Record Relationships | No | Yes (to metadata & objects) | No |
| Data Visibility | All users can query | All users can query | All users can access |
| Runtime Modification | Yes (via DML/API) | No (metadata API only) | No |
| Multi-Language Support | No | No | Yes (127 languages) |
| Maximum Records | ~300 settings with data limits | Up to 50,000 records per type | 5,000 labels |
| Validation Rules | No | Yes | No |
| CI/CD Friendly | Partial (structure only) | Excellent | Excellent |
| Test Data Creation | Easy (DML in tests) | Requires setMock() or test records | Not needed (always available) |
| Performance | Excellent (application cache) | Good (platform cache + SOQL) | Excellent (application cache) |
| Field Types Supported | Standard field types | Standard field types + metadata relationships | Text only |
| Security Model | Profile/user hierarchy | Same as other metadata | Same as other metadata |
Decision-Making Guidelines: When to Use What

Use Custom Settings When:
- You need user or profile-specific configuration overrides
- Example: Territory-based discount percentages that vary by sales rep
- Example: Feature flags that enable different capabilities by profile
- Configuration needs runtime modification via UI
- Example: Admin-managed thresholds in a custom app
- Example: Temporary feature toggles changed frequently
- You’re working with legacy code that already uses them
- Refactoring existing Custom Settings to CMDTs requires cost-benefit analysis
- You need extremely fast reads without SOQL queries
- Custom Settings’
getInstance()is marginally faster than CMDT queries
- Custom Settings’
Use Custom Metadata Types When:
- Configuration must deploy with code (modern best practice)
- Example: Integration endpoints that differ by environment
- Example: Validation rules that should match application version
- You need relationships to other metadata or objects
- Example: Field mappings between objects
- Example: Dynamic routing based on record types
- Building managed packages requiring protected configuration
- Example: ISV apps with customer-customizable behavior
- Implementing plugin/strategy patterns
- Example: Payment gateway configurations with handler classes
- Example: Notification channels with different implementations
- You want comprehensive test coverage without test data dependencies
- Use
Test.createStub()for Custom Metadata in tests
- Use
- Creating SaaS-style multi-tenant configurations
- Example: Feature matrix for different subscription tiers
Use Custom Labels When:
- Storing user-facing text requiring translation
- Example: Button labels, error messages, help text
- Example: Email templates with merge fields
- You need centralized message management
- Example: Consistent terminology across components
- Example: Brand-specific messaging
- Supporting multiple languages is a requirement
- Custom Labels are the only native i18n solution
- Externalizing text from code for non-technical stakeholders
- Example: Marketing team managing promotional messages
- Example: Legal team controlling disclaimer text
Use None of These When:
- Large datasets: Use Custom Objects or Big Objects
- Transactional data: Use Standard/Custom Objects
- Sensitive secrets: Use Named Credentials or Protected Custom Settings
- Complex relationships: Use Custom Objects with proper architecture
Common Mistakes and Anti-Patterns

Mistake 1: Using Custom Labels for Logic
apex// ❌ WRONG
if (opportunityStage == System.Label.Closed_Won_Stage) {
// Logic depends on user-editable text
}
// ✅ CORRECT
if (opportunityStage == Opportunity_Config__mdt.getInstance('Default').Closed_Won_Stage__c) {
// Logic uses typed configuration
}
Mistake 2: Not Caching Custom Metadata Queries
apex// ❌ INEFFICIENT
public void processRecords(List<Account> accounts) {
for (Account acc : accounts) {
// This queries CMDT on every iteration!
Integration_Config__mdt config = [SELECT Endpoint__c FROM Integration_Config__mdt LIMIT 1];
}
}
// ✅ EFFICIENT
public void processRecords(List<Account> accounts) {
Integration_Config__mdt config = [SELECT Endpoint__c FROM Integration_Config__mdt LIMIT 1];
for (Account acc : accounts) {
// Reuse cached config
}
}
Mistake 3: Overusing Hierarchy Custom Settings
apex// ❌ COMPLEXITY TRAP
// Having 20+ hierarchy settings creates maintenance nightmare
API_Config__c.getInstance().Endpoint__c
API_Config__c.getInstance().Timeout__c
Feature_Flags__c.getInstance().Enable_Feature_A__c
Feature_Flags__c.getInstance().Enable_Feature_B__c
// ... etc
// ✅ BETTER APPROACH
// Use Custom Metadata with selective hierarchy where needed
Integration_Configs__mdt configs = Integration_Configs__mdt.getAll();
Mistake 4: Mixing Configuration Strategies
Maintain consistency—don’t use Custom Settings for API config in one class and Custom Metadata in another for the same logical domain.
Best Practices for Salesforce Configuration
1. Establish Configuration Strategy Early
Document your org’s configuration standards:
- Custom Metadata Types: Default for app configuration
- Custom Settings: Only for user/profile-specific overrides
- Custom Labels: Exclusively for UI text
2. Use Descriptive Naming Conventions
apex// Good naming makes purpose clear
Email_Service_Config__mdt // Configuration
Feature_Access__c // Hierarchy Custom Setting (note __c)
Err_InvalidEmailFormat // Custom Label (prefix for category)
3. Implement Configuration Validation
apexpublic class ConfigValidator {
public static void validateIntegrationConfigs() {
for (Integration_Config__mdt config : Integration_Config__mdt.getAll().values()) {
if (String.isBlank(config.Endpoint__c)) {
throw new ConfigException('Invalid config: ' + config.DeveloperName);
}
if (!config.Endpoint__c.startsWith('https://')) {
throw new ConfigException('Endpoint must use HTTPS: ' + config.DeveloperName);
}
}
}
}
4. Version Control Everything
Treat configuration as code:
- Custom Metadata records in source control
- Custom Settings structure in source control
- Data migration scripts for Custom Settings data
- Custom Labels tracked with translations
5. Test Configuration Dependencies
apex@IsTest
private class NotificationRouterTest {
@IsTest
static void testWithMockMetadata() {
// Create stub for Custom Metadata
Notification_Channel__mdt mockChannel = new Notification_Channel__mdt(
Channel_Name__c = 'Email',
Handler_Class__c = 'EmailNotificationHandler',
Max_Retries__c = 3
);
// Test logic with mocked config
// Implementation depends on abstraction layer
}
}
6. Document Configuration Dependencies
Maintain a configuration map showing:
- Which classes depend on which configuration
- Environment-specific values
- Migration paths between configuration types
Migration Strategies
Migrating from Custom Settings to Custom Metadata
apex// 1. Export Custom Settings data
List<API_Config__c> settings = API_Config__c.getAll().values();
// 2. Transform to Custom Metadata format (manual process)
// Create corresponding CMDT records via Metadata API or Setup UI
// 3. Update code incrementally
// Old: API_Config__c.getInstance().Endpoint__c
// New: API_Config__mdt.getInstance('Default').Endpoint__c
// 4. Deploy and test in sandbox
// 5. Migrate production
// 6. Deprecate Custom Settings after validation period
Conclusion
The choice between Salesforce Custom Metadata vs Custom Settings vs Custom Labels fundamentally impacts your application’s architecture, deployability, and maintainability.
Modern best practice strongly favors Custom Metadata Types for application configuration due to their superior deployment characteristics and metadata-first approach. Custom Settings remain valuable for profile-specific overrides and rapid administrative changes. Custom Labels exclusively serve internationalization needs.
The key is architectural discipline: establish clear conventions early, document your decisions, and resist the temptation to mix approaches without justification. As Salesforce continues evolving toward metadata-driven development, Custom Metadata Types increasingly represent the path forward for scalable, enterprise-grade configuration management.
Your configuration strategy isn’t just a technical decision—it’s a statement about your organization’s commitment to DevOps maturity, maintainability, and long-term platform health. Choose wisely, implement consistently, and your future self (and teammates) will thank you.
About RizeX Labs
At RizeX Labs, we specialize in delivering scalable and efficient Salesforce solutions that solve real business problems—not theoretical ones. Our expertise spans across platform customization, automation, and architecture design using tools like Custom Metadata, Custom Settings, and Custom Labels.
We focus on building maintainable and deployment-friendly systems that reduce technical debt and improve long-term performance. Instead of quick fixes, we implement solutions that scale with your business.
Internal Links:
- Link to your Salesforce course page
- How to Build a Salesforce Portfolio That Gets You Hired (With Project Ideas)
- Salesforce Admin vs Developer: Which Career Path is Right for You in 2026?
- Wealth Management App in Financial Services Cloud
- Enroll in Salesforce Dev batch
External Links:
- Salesforce official website
- SOQL and SOSL reference
- Salesforce Developer Guide
- Salesforce Trailhead (SOQL modules)
- Salesforce Object Reference
Quick Summary
When choosing between Salesforce configuration mechanisms, Custom Metadata Types represent the modern best practice for application configuration, offering full metadata deployment (structure and data together), making them ideal for CI/CD pipelines, managed packages, and complex business rules with metadata relationships. Custom Settings remain valuable for profile/user-specific configuration hierarchies and scenarios requiring runtime admin modification through the UI, though they only deploy structure as metadata—not the data itself. Custom Labels serve exclusively for user-facing text and internationalization, supporting up to 127 languages through Translation Workbench. The key architectural decision: use Custom Metadata Types as your default for application config, reserve Custom Settings for genuine hierarchy needs or legacy compatibility, and limit Custom Labels strictly to UI text—never for business logic. Modern Salesforce development strongly favors Custom Metadata Types due to superior deployability, versioning, and DevOps maturity, while Custom Settings' hierarchy feature and cached getInstance() method remain unmatched for user-specific overrides. Mixing these approaches without clear standards creates technical debt; establish org-wide conventions early, document configuration dependencies, and treat all configuration as code within your version control and CI/CD processes for long-term maintainability and scalability.
