PR Validation 10x Slower: Solving Permission Set Bottlenecks
If your Pull Request (PR) validation times in Salesforce have ballooned from a few minutes to nearly an hour, you are not alone. As orgs grow in complexity, the interaction between metadata deployments and the platform's security model turns into a performance problem that is easy to miss. Bundling PermissionSet metadata (especially classAccesses) with Apex class deployments puts the Salesforce metadata engine under the heaviest strain.
The anatomy of the bottleneck
When you include PermissionSet or PermissionSetGroup metadata in a deployment alongside Apex classes, the Salesforce metadata API has to do a heavy lift. Salesforce does not simply "move" the XML files; it validates the integrity of the security model across your entire org.
The calculation chain
- Metadata parsing. The platform parses the new
classAccessesentries in your Permission Set XML. - PSG recalculation. If those Permission Sets belong to a Permission Set Group (PSG), Salesforce starts an asynchronous recalculation job so that the effective permissions of the user reflect the new state.
- Apex dependency check. The compiler verifies that every class being deployed or modified has the required security context. If your tests use
System.runAs(), the platform is forced to generate a synthetic user session that honors these new permissions.
When these processes collide, the metadata lock is held significantly longer, and runAs test execution slows sharply as the CPU constantly re-evaluates the permission cache for every test context.
The runAs and test context trap
Developers often overlook how System.runAs() makes performance worse during deployment validation. When you execute code within runAs in a test class, the platform checks the current user's permission set cache.
@isTest
private class PermissionPerformanceTest {
@isTest
static void testAccessWithNewPermissions() {
User testUser = [SELECT Id FROM User WHERE Alias = 'perf'];
// In a deployment, this triggers a permission cache lookup.
// If PSG/PermissionSet metadata is currently being updated,
// this call can wait on a metadata lock.
System.runAs(testUser) {
MyServiceClass.executeSensitiveLogic();
}
}
}
If your deployment includes changes to the classAccesses of the Permission Set assigned to testUser, the security engine may be forced to flush and rebuild the cache repeatedly while the test suite runs. That is the primary driver of the "10x slower" phenomenon.
Decoupling metadata deployments
To restore your pipeline performance, use a "decoupled metadata" strategy: treat security metadata as a separate release tier from your application logic.
Strategy 1: The deployment split
Instead of one massive deployment, split your PRs into two distinct pipelines:
- Pipeline A (logic): Apex classes, triggers, and Lightning components. No security metadata changes.
- Pipeline B (security): Permission Sets, PSGs, and Profiles. Run it only when security configuration changes are required.
Strategy 2: Use Permission Sets instead of Profiles
Avoid profile-level access modifications where you can. Profile metadata is heavy because it includes page layout assignments, field-level security, and tab settings. Migrating your security to dedicated PermissionSets reduces the surface area of metadata that has to be recalculated during Apex deployments.
Strategy 3: Optimize runAs for tests
Rather than testing full permission sets in every single unit test, use a "Permission Service" pattern to mock permissions where that is valid, or limit the scope of your runAs blocks.
public class SecurityMockUtil {
// Use this to check access instead of relying on complex runAs scenarios
// during bulk testing to speed up validation
public static Boolean hasAccess() {
return FeatureManagement.checkPermission('Allow_Sensitive_Feature');
}
}
Monitoring the metadata lock
If you suspect a specific deployment is hitting a wall, query MetadataDeployStatus through the Tooling API to see whether your validation spends most of its time in the Pending or InProgress state on security components.
// Querying deployment status via Tooling API
SELECT Id, Status, NumberComponentsTotal, NumberComponentsDeployed
FROM MetadataDeployStatus
WHERE CreatedById = 'YOUR_USER_ID'
ORDER BY CreatedDate DESC LIMIT 1
A high NumberComponentsTotal with Status stuck at InProgress for extended periods points to PSGs in your file manifest (package.xml) that have many members. The larger the PSG, the longer the recalculation delay.
Leave a Comment