Skip to main content
SFDC Developers
Apex

PR Validation 10x Slower: Solving Permission Set Bottlenecks

Vinay Vernekar · · 5 min read

PR Validation 10x Slower: Solving Permission Set Bottlenecks

If you have recently noticed that your Pull Request (PR) validation times in Salesforce have ballooned from a few minutes to nearly an hour, you are not alone. As our orgs grow in complexity, the interaction between metadata deployments and the platform’s security model becomes a hidden performance killer. Specifically, bundling PermissionSet (especially classAccesses) with Apex class deployments creates a perfect storm for the Salesforce metadata engine.

In this guide, we will break down why this happens, how the Permission Set Group (PSG) recalculation engine interacts with Apex, and the architectural changes you can implement to regain your deployment velocity.

The Anatomy of the Bottleneck

When you include PermissionSet or PermissionSetGroup metadata in a deployment alongside Apex classes, the Salesforce metadata API must perform a heavy lift. Salesforce does not simply "move" the XML files; it must validate the security model integrity across your entire org.

The Calculation Chain

  1. Metadata Parsing: The platform parses the new classAccesses entries in your Permission Set XML.
  2. PSG Recalculation: If those Permission Sets belong to a Permission Set Group (PSG), Salesforce initiates an asynchronous recalculation job. This ensures that the effective permissions of the user reflect the new state.
  3. Apex Dependency Check: The compiler must verify that every class being deployed or modified has the required security context. If you are using System.runAs() in your tests, 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 the runAs test execution becomes exponentially slower as the CPU must constantly re-evaluate the permission cache for every test context.

The runAs and Test Context Trap

Developers often overlook how System.runAs() exacerbates performance issues 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 underlying security engine may be forced to flush and rebuild the cache repeatedly during the test suite execution. This is the primary driver of the "10x slower" phenomenon.

Decoupling Metadata Deployments

To restore your pipeline performance, we recommend a "decoupled metadata" strategy. You should 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. Only run this when security configuration changes are required.

Strategy 2: Use Permission Sets, Not Profiles

Avoid profile-level access modifications if possible. Profile metadata is notoriously heavy because it includes page layout assignments, field-level security, and tab settings. By migrating your security to dedicated PermissionSets, you reduce the surface area of the metadata that needs to be recalculated during Apex deployments.

Strategy 3: Optimize runAs for Tests

Rather than testing full permission sets for every single unit test, use a "Permission Service" pattern to mock permissions where 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, you can use the MetadataDeployStatus (via Tooling API) to monitor if your validation is spending most of its time in the Pending or InProgress state regarding 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

If you see high NumberComponentsTotal but the Status stays at InProgress for extended periods, look for PSGs in your file manifest (package.xml) that have many members. The larger the PSG, the longer the recalculation delay.

Key Takeaways

  • Avoid Bundling: Do not include PermissionSet or PermissionSetGroup metadata in the same deployment as your core Apex business logic.
  • Understand PSG Recalculation: Every time you touch a PSG in metadata, Salesforce triggers a background recalculation job that effectively "locks" the permission state for that org.
  • Optimize Test Contexts: Excessive use of System.runAs() combined with frequent security metadata updates forces the platform to rebuild permission caches, causing massive latency in your CI/CD pipeline.
  • Modularize package.xml: Maintain separate manifest files for security and logic to keep validation times lean and predictable.
  • Monitor Metadata API: Use the Tooling API to verify if your deployments are hanging on security components and adjust your release cadence accordingly.

Share this article

Get weekly Salesforce dev tutorials in your inbox

Comments

Loading comments...

Leave a Comment

Trending Now