Not verified hands-on. This article was written from primary documentation rather than reproduced in an org, so it carries no tested-release badge. If you run it and something differs, say so in the comments and it gets tested and corrected.

The short version: stop calling Schema.getGlobalDescribe(). It builds a map of every object in the org, which on a mature org with managed packages is thousands of entries, and it costs real CPU and heap every time you touch it. Use a token you already have, Type.forName when the name is dynamic, and cache anything you look up more than once. Record type ids should never be hardcoded, and getting them by developer name is a one liner.

Why getGlobalDescribe is the wrong default

The method returns Map<String, Schema.SObjectType> covering every standard, custom and packaged object. You almost always want one entry from it. On an org with a few managed packages installed, the map runs to several thousand keys, and building it consumes a meaningful slice of your 10,000 ms synchronous CPU budget along with a chunk of the 6 MB heap.

Calling it inside a loop over trigger records is one of the classic slow trigger causes, and it does not throw a limit exception. It just makes everything take longer until CPU time runs out somewhere unrelated.

If you need an SObjectType from a string name, this is the cheap way:

Schema.SObjectType t = ((SObject) Type.forName('Account').newInstance())
    .getSObjectType();

Or, when the name may carry a namespace and you want the describe directly:

List<Schema.DescribeSObjectResult> results =
    Schema.describeSObjects(new List<String>{ 'Account', 'Contact' });

describeSObjects takes a list, so one call covers every object you care about. That is the pattern to reach for when the object names come from custom metadata or configuration.

Tokens, and why they are cheap

An SObjectType is a lightweight token, not a describe result. Account.SObjectType and Account.Industry compile to tokens directly, so there is no runtime lookup at all. Tokens are also the compiler’s way of telling you a field was renamed, which a string never will.

Schema.SObjectType acc = Account.SObjectType;
Schema.SObjectField ind = Account.Industry;

Boolean canEdit = ind.getDescribe().isUpdateable();
String label     = acc.getDescribe().getLabel();

The cost arrives at getDescribe(), which loads labels, field maps and relationship information. Pull it once into a local variable rather than chaining it repeatedly in a loop.

Where you only need a shallow answer, ask for it lazily:

Schema.DescribeSObjectResult d =
    Account.SObjectType.getDescribe(SObjectDescribeOptions.DEFERRED);

DEFERRED skips loading the field map and child relationships until something actually asks for them. If all you want is isAccessible() or the label, it is markedly cheaper than the default.

Record type ids without hardcoding

A record type id is different in every org, so a hardcoded one works in the sandbox it was written in and nowhere else. It is also 18 characters of nothing, which makes the code unreadable.

Id businessRt = Schema.SObjectType.Account
    .getRecordTypeInfosByDeveloperName()
    .get('Business_Account')
    .getRecordTypeId();

Use getRecordTypeInfosByDeveloperName(), not getRecordTypeInfosByName(). The label is translatable and an admin can change it without warning. The developer name is stable and is what the metadata actually keys on. This mistake produces a null pointer exception the first time someone enables a second language.

Going the other way, from a record on hand:

Schema.RecordTypeInfo info = Schema.SObjectType.Account
    .getRecordTypeInfosById().get(acc.RecordTypeId);
String devName = info.getDeveloperName();

Cache the map in a static. Record type configuration cannot change mid transaction, so there is no reason to look it up twice.

The caching pattern

A small static holder removes almost all of the cost and gives you one place to look.

public class DescribeCache {
    private static Map<String, Schema.SObjectField> accFields;

    public static Schema.SObjectField accountField(String name) {
        if (accFields == null) {
            accFields = Account.SObjectType.getDescribe().fields.getMap();
        }
        return accFields.get(name.toLowerCase());
    }
}

Note the toLowerCase(). The field map is keyed on the lowercase API name, and a lookup with the cased version returns null. That detail costs people an afternoon roughly once per career.

Statics live for the transaction, not beyond it, so this saves you within one execution. For genuinely cross request caching, put the result in the platform cache.

Measured in a Summer ’26 scratch org on 14 September 2026: Schema.getGlobalDescribe() returned 1,390 object types in 108 ms. Calling it once per record in a loop is how a transaction reaches the CPU limit without doing any work.

Frequently asked

Is there a governor limit on describe calls? Not a documented count limit any more. The constraint is CPU and heap, which is worse in a way, because you get slow code instead of a clear failure.

How do I check field level security from a describe? isAccessible(), isCreateable() and isUpdateable() on the field describe. For queries and DML, prefer WITH USER_MODE and the AccessLevel overloads, which do the whole check for you. WITH SECURITY_ENFORCED is no longer supported.

Can I detect whether an object exists before referencing it? Yes. Type.forName('Namespace__Object__c') returns null when nothing matches, which is the standard way for a package to handle an optional dependency without failing to compile.

Does the field map include fields the user cannot see? Yes. Describe returns the metadata regardless of permissions, and the accessibility flags are how you find out what the running user may do with it.