The short answer: Schema.getGlobalDescribe() builds a map of every object in the org, and calling it twice costs twice. Cache the results you need in a lazily populated static, and prefer the targeted describe calls over the global one wherever you can name the object up front. On a large org this is often the single biggest CPU saving available in a trigger.
Why the global describe is expensive
getGlobalDescribe() returns a map of API name to SObjectType for every standard and custom object visible to the running user. On an org with a few managed packages that is well over a thousand entries, and building the map is not free in CPU or heap.
Called once in a transaction, it is a cost you absorb. Called inside a loop over 200 records, it is the reason the transaction dies at 10,000ms with no obvious culprit on the stack.
Field describes have the same shape. getDescribe().fields.getMap() on a wide object returns hundreds of entries, and code that calls it per record to check one field’s accessibility is doing that work hundreds of times for one answer.
The lazy static
Statics live for the transaction. That is exactly the right lifetime for describe data, because schema cannot change mid-transaction.
public class DescribeCache {
private static Map<String, Schema.SObjectType> globalMap;
private static Map<String, Map<String, Schema.SObjectField>> fieldMaps
= new Map<String, Map<String, Schema.SObjectField>>();
public static Schema.SObjectType objectType(String apiName) {
if (globalMap == null) {
globalMap = Schema.getGlobalDescribe();
}
return globalMap.get(apiName);
}
public static Map<String, Schema.SObjectField> fields(String apiName) {
if (!fieldMaps.containsKey(apiName)) {
fieldMaps.put(apiName,
objectType(apiName).getDescribe().fields.getMap());
}
return fieldMaps.get(apiName);
}
}
The first caller pays. Every caller after that gets a map lookup. In a trigger that checks field accessibility for 200 records, that turns 200 describes into one.
Avoid the global describe entirely where you can
Most code that calls getGlobalDescribe() already knows which object it wants, which makes the global call the wrong tool.
// Costs a full global describe
Schema.SObjectType t = Schema.getGlobalDescribe().get('Account');
// Does not
Schema.SObjectType t = Account.SObjectType;
// From an existing record, also cheap
Schema.SObjectType t = record.getSObjectType();
// Single field describe, no field map built
Schema.DescribeFieldResult f = Account.Industry.getDescribe();
The static reference Account.SObjectType is resolved by the compiler and costs essentially nothing. Reach for getGlobalDescribe() only when the object name genuinely arrives at runtime, from custom metadata or user input, and even then cache the result.
What is worth caching
- The global describe map, when you need it at all.
- Field maps for objects you inspect repeatedly, such as the object a generic utility processes.
- Picklist value lists.
getPicklistValues()allocates a list of wrapper objects every call, and picklist rendering code loves to call it per row. - Record type Id to developer name mappings, which are a describe call underneath and get looked up constantly.
- Field accessibility results for the specific fields a class checks, resolved once to a Set of names.
What not to cache
- Anything user-specific across transactions. Field accessibility and object permissions depend on the running user. A transaction-scoped static is safe because one transaction has one user. Putting the same data in Platform Cache, which is shared, leaks one user’s permissions to another.
- Describe results in a stateful batch that runs for hours. Schema can be deployed mid-job, and stale describes produce confusing errors.
- Query results. A static holding records from earlier in the transaction goes stale as soon as anything updates them. Caching describes is safe because the schema is immutable within the transaction. Caching data is not the same problem.
- Large field maps you use once. Caching costs heap. A map you touch a single time should not be kept.
Measuring the saving
Integer cpu = Limits.getCpuTime();
Map<String, Schema.SObjectType> m = Schema.getGlobalDescribe();
System.debug(LoggingLevel.ERROR, 'global describe cpu: ' +
(Limits.getCpuTime() - cpu) + ', objects: ' + m.size());
Run that in execute anonymous against your own org rather than trusting any published figure, because the cost scales with how many objects you have. Then multiply by the number of times your trigger calls it. That multiplication is usually where the argument ends.
Measured in a Summer ’26 scratch org on 14 September 2026: a single Schema.getGlobalDescribe() returned 1,390 object types, took 108 ms, and left the transaction at 122 ms of CPU from a near-zero start. That is more than one per cent of the 10-second synchronous budget for one call, before your code has done anything.
Frequently asked
Does the platform cache describes for me? There is internal caching, and it is not something to rely on. A static in your own code is predictable and costs one line.
Is getGlobalDescribe() a governor limit? Describe calls have their own limit, and CPU time is the constraint you will hit first.
Can I put describes in Platform Cache? Schema objects are not serialisable into it. Derived values such as a Set of field name strings can be cached, provided they are not user-dependent.
Does this matter in a small org? Less. The cost scales with object count, so an org with 60 objects and no packages will see a much smaller number than one with 1,500.
