Scalability Patterns
Learn more about Well-Architected Reliability → Scalability and Capacity Planning
Patterns
| Where to look | What good looks like |
|---|---|
| SOQL query efficiency | Consolidate queries using relationship queries (Contacts, Cases__r) instead of separate queries per related object. Query all needed data once per transaction. Target <50 queries in synchronous transactions, <100 in asynchronous (limits: 100 sync, 200 async). Maintain 30-50% buffer below limits. |
| DML operation bulkification | Collect records into Lists, execute single DML statement per object type per transaction. Use Database.insert/update/delete with allOrNone parameter controlling partial success behavior. Target <75 DML statements per transaction (limit: 150). Batch reduces DML consumption from N statements to 1. |
| Heap size management | Query only required fields (avoid SELECT * patterns). Process large result sets in chunks. Clear collections after processing to free memory. Target <4MB heap in synchronous context (limit: 6MB), <8MB asynchronous (limit: 12MB). Heap exceeded errors indicate need for chunking or streaming patterns. |
| CPU time optimization | Minimize computational complexity in loops. Use Maps for lookups (O(1)) instead of nested List iteration (O(n²)). Offload heavy computation to asynchronous context. Target <7,000ms CPU in synchronous transactions (limit: 10,000ms), <40,000ms asynchronous (limit: 60,000ms). |
| SOQL query rows | Design queries returning reasonable result set sizes. Use LIMIT clause to cap results. Target <25,000 rows per query in synchronous context (limit: 50,000). Queries approaching limits should use pagination or move to asynchronous processing with Batch Apex or Queueable. |
| API call allocation | Monitor 24-hour API limit consumption via System Overview. Batch API operations instead of record-by-record calls. Cache frequently accessed reference data using Platform Cache. Target <70% of daily API limit to accommodate spikes. API limit varies by edition and license count. |
| Callout limits | Consolidate external callouts: batch multiple records into single API request when external system supports it. Implement request queuing to serialize callouts. Respect 120-second total callout time per transaction. Limit: 100 callouts per transaction. Design async processing for callout-heavy integrations. |
| Code (Apex, Aura, LWC) | Implement bulkification patterns: process collections not individual records, query once and cache in Maps, execute single DML per object type. Use relationship queries to minimize SOQL consumption. Apply efficient algorithms avoiding nested loops on large data sets. |
Anti-Patterns
| Where to look | What bad looks like |
|---|---|
| SOQL in loops | Problem: Executing SOQL queries inside for loops processing record collections. Impact: Query limit exceeded error at 100+ records (limit: 100 queries per transaction). Resolution: Query once before loop, store results in Map keyed by relationship field. Lookup from Map inside loop (bulkification pattern). |
| DML in loops | Problem: Executing insert/update/delete statements inside for loops. Impact: DML limit exceeded error at 150+ records (limit: 150 DML statements per transaction). Resolution: Collect records into List inside loop, execute single DML statement after loop completes. |
| No governor limit buffer | Problem: Designing transactions consuming 95%+ of governor limits under normal conditions. Impact: Production failures during slight load increases, no capacity for edge cases. Resolution: Target 70% of limits as operational ceiling. Reserve 30% buffer for spikes and edge cases. |
| Nested loop cartesian products | Problem: Nested loops iterating two large collections creating O(n²) operations. Impact: CPU limit exceeded errors, heap limit exceeded from temporary objects. Resolution: Use Map-based lookups converting O(n²) to O(n). Collect outer loop keys, query related records once, lookup in inner loop. |
| Heap accumulation | Problem: Loading large query results into memory without processing in chunks. Impact: Heap limit exceeded error (6MB sync, 12MB async). Resolution: Query only needed fields. Process in batches clearing collections after each batch. Use QueryLocator with Batch Apex for multi-million record processing. |
| No field-level filtering | Problem: Selecting all fields (explicit or implicit) when only subset needed. Impact: Excessive heap consumption, slower query execution, SOQL rows limit approached unnecessarily. Resolution: SELECT only fields required for processing. Use relationship notation to fetch related data in single query rather than separate queries. |
| Synchronous processing overuse | Problem: Processing large data volumes synchronously within user transactions. Impact: User waiting for multi-second operations, CPU/heap limits exceeded blocking user. Resolution: Move heavy processing to async context (Batch, Queueable, Platform Events). Provide user immediate response with status polling. |
Patterns
| Where to look | What good looks like |
|---|---|
| Asynchronous processing | Use Batch Apex for processing large record volumes (millions). Use Queueable Apex for complex multi-step orchestration with chaining. Use Platform Events for event-driven decoupling. Use Scheduled Apex for periodic maintenance. Match async pattern to use case: Batch for volume, Queueable for complexity, Events for decoupling. |
| Batch Apex implementation | Design start method returning selective QueryLocator to minimize records processed. Configure batch size based on record complexity (200-2,000 records per execute). Implement stateful interface only when necessary as it reduces throughput. Monitor AsyncApexJob for success/failure. Batch provides dedicated governor limits per chunk. |
| Platform Events architecture | Publish events for state changes requiring downstream processing. Subscribe using Apex triggers on platform event objects or external subscribers via CometD. Design idempotent subscribers (replay may deliver events multiple times). Use 72-hour retention for recovery from temporary subscriber failures. |
| Date-based partitioning | Process data in time windows: current month, last quarter, this year. Archive historical data to Big Objects or external storage. Time-based partitioning enables parallel batch jobs processing different date ranges simultaneously. Most queries naturally filter by date making this efficient. |
| Record type partitioning | Process different record types independently. Schedule separate batch jobs per record type enabling parallelization. Record types often correlate with distinct business processes justifying independent processing. Maximum 5 concurrent batch jobs per org requires strategic partitioning. |
| Geography-based partitioning | Partition by owner, division, or territory for processing distribution. Process each region, business unit, or sales territory in parallel batch jobs. Geography-based partitioning leverages sharing model: each batch job runs in context of specific user group naturally filtering data. |
| Caching strategy | Use Platform Cache (org/session partitions) for frequently accessed reference data. Cache reduces query and API consumption. Allocate org and session partitions in whole-MB increments; default capacity varies by edition, with additional capacity purchasable in blocks. Cache miss should gracefully fall back to query. Cache TTL should match data change frequency: hours for static reference data, minutes for semi-dynamic. |
| Flow bulkification | Design Flows processing record collections using Loop and Collection variables. Create collection of records, perform single Get Records, update collection with single Update Records element. Avoid individual record operations inside loops. Flow bulkification patterns mirror Apex best practices. |
Anti-Patterns
| Where to look | What bad looks like |
|---|---|
| Sequential processing only | Problem: Single batch job processing all 50 million records sequentially taking 24+ hours. Impact: Blocks other processing, maintenance window exhaustion, user-facing delays. Resolution: Partition data by date, record type, or geography. Run parallel batch jobs processing independent partitions simultaneously. |
| Synchronous integration dependencies | Problem: User transactions blocked waiting for external system responses via synchronous callouts. Impact: User experience tied to external system performance, timeout failures during external outages. Resolution: Use Platform Events for asynchronous integration. Decouple user transaction from external system availability. |
| No caching layer | Problem: Querying same reference data repeatedly across transactions. Impact: Excessive SOQL consumption, API limit waste, slower transaction execution. Resolution: Implement Platform Cache for frequently accessed reference data. Cache reduces governor limit consumption and improves performance. |
| Batch job stacking | Problem: Scheduling 20+ batch jobs starting simultaneously. Impact: Queue depth buildup, 5 concurrent job limit causes serialization, unpredictable completion times. Resolution: Stagger schedules distributing load across hours. Consolidate similar jobs into single batch with dynamic dispatch logic. |
| Stateful batch overuse | Problem: Implementing Batch Apex with Database.Stateful when not required. Impact: Unnecessary serialization of instance member variables across execute() invocations and potential heap accumulation from retained state. Resolution: Use stateful only when state must persist across execute invocations. Prefer stateless batches to avoid unnecessary state-serialization overhead. |
| Single-threaded Flow design | Problem: Flow processing records one-by-one inside Loop with individual Get/Update Record elements per iteration. Impact: Governor limit consumption proportional to record count, transaction failures at scale. Resolution: Collect records into collection variable, perform single Get Records, update collection with single Update Records element outside loop. |
| No async retry strategy | Problem: Failed async jobs not automatically retried, requiring manual intervention. Impact: Data inconsistencies, manual operational burden, SLA misses. Resolution: Implement automatic retry with exponential backoff. Use Platform Events for durable queue enabling replay. Monitor AsyncApexJob and alert on sustained failures. |
Patterns
| Where to look | What good looks like |
|---|---|
| Batch vs Queueable selection | Use Batch Apex for: processing millions of records in chunks, scheduled bulk operations, data migrations. Use Queueable Apex for: multi-step integration workflows with chaining, operations requiring complex object parameters, jobs needing better monitoring than @future. Async Apex shares a limit of 250,000 executions per 24 hours (DailyAsyncApexExecutions) across Batch, Queueable, Future, and Scheduled Apex, rather than a Queueable-specific allocation. |
| Queueable chaining | Implement multi-step workflows by enqueuing next job from current job’s execute method. Chain enables sequential processing: callout → parse response → enqueue follow-up action. Maximum 1 enqueue per queueable execution prevents runaway chains. Design chains with completion detection to avoid infinite loops. |
| @future method usage | Use @future for simple async operations requiring no complex parameters (primitives only) and no chaining. Limit: 50 @future calls per Apex transaction. @future provides simplest async pattern but lacks visibility: no job monitoring, no chaining, no complex parameters. Prefer Queueable for new development. |
| Scheduled Apex optimization | Consolidate scheduled operations into single schedulable class dispatching to specific handlers. Maximum 100 scheduled jobs per org requires strategic consolidation. Schedule during off-hours to reduce contention with user operations. Use Scheduled Apex for orchestration, delegate heavy processing to Batch Apex. |
| Platform Event replay | Design subscribers handling replay scenarios: duplicate events after subscriber failure, gap events after extended outage. Maintain last processed ReplayId per subscriber. Use EventBus.getReplayId() to detect replay vs new events. Implement idempotent processing (processing same event twice produces same result). |
| Continuation pattern | Use Continuation for long-running callouts from Lightning components requiring >5 second response times. Continuation enables multiple simultaneous 120-second callouts improving perceived performance for parallel operations. Limit: 3 continuations per request. Implement timeout handling and user feedback. |
For inline examples and detailed guidance, see the Reliability pillar, or browse the Patterns and Anti-Patterns explorer.