“The instance is slow” is one of the least useful sentences in platform work, and one of the most common. It is also almost never about infrastructure — the hardware is fine, and asking for a bigger instance is the ServiceNow equivalent of restarting the router.
What it usually is: a handful of specific, findable things doing far more work than anyone realises. Here they are, roughly in the order I find them, followed by how to actually locate yours.
1. GlideRecord queries inside loops
This is the biggest one by a wide margin, and it survives in production for years because it works perfectly on a developer’s instance with forty records.
The shape: iterate a result set, and inside the loop, query another table to look something up. Five hundred records means five hundred additional queries, each with its own round trip and ACL evaluation.
Two fixes cover nearly every case.
When you need a count or a sum, use GlideAggregate. Counting by querying and iterating a result set brings every row back to count them. Aggregation happens in the database, where it belongs.
When you need related data, query once and build a map. One query for all the related records, assembled into an object keyed by the field you are matching on, then look up inside the loop. Two queries total instead of five hundred and one. This is the single highest-value refactor available in most instances.
2. Conditions the database cannot index
Not all query operators are equal, and the difference is the difference between an index seek and a full table scan.
CONTAINS cannot use an index. It compiles to a leading-wildcard match, which forces the database to read every row. On a table with millions of rows this is exactly as slow as it sounds. STARTSWITH can use an index and is a valid substitute far more often than people assume.
Conditions on unindexed custom fields are the other frequent offender. A custom field used in a query that runs on every form load, every list, or inside a business rule deserves an index — and if it is being filtered constantly, it probably deserves one regardless.
Broad OR conditions frequently defeat index usage entirely. Where a query has grown a chain of ORs, restructuring into separate queries is often dramatically faster despite looking less elegant.
3. Business rules with no condition
A business rule with an empty condition runs on every insert and update on that table, forever, and then evaluates its own internal if to decide it has nothing to do.
On a busy table that is a permanent tax. The fix costs nothing: put the condition in the condition field, where the platform can skip the rule entirely rather than executing a script to learn it should have.
This is one of several reasons where you put logic matters as much as what it does — the same behaviour expressed in the right place is often an order of magnitude cheaper.
4. Synchronous integrations inside transactions
An outbound REST call inside a synchronous business rule means the user’s save is waiting on someone else’s infrastructure. When that endpoint is having a bad day, so is everyone on your instance — and connection pool exhaustion turns one slow partner into an instance-wide problem.
Integrations belong in async rules, scheduled jobs, or flows with proper error handling. Almost nothing genuinely requires a third-party response before a record can be saved.
5. Table extension and the journal problem
The task table hierarchy is convenient and it means your queries touch a very large shared table. Extended tables are stored across a parent and child, so a poorly-conditioned query against a task-derived table can read far more than the records you care about.
The specific table worth watching is sys_journal_field — every comment and work note ever written, on every record, in one place. It grows without bound and it is queried constantly, because activity streams are on every form. On long-lived instances this is routinely among the largest tables and a significant share of total query time.
Which leads to the thing almost nobody does: archive. Instances running for a decade with every closed record and every journal entry from 2016 still live are paying for that data on every query. A retention policy is not glamorous work, and on old instances it is frequently the single largest available win.
6. ACL evaluation at scale
ACLs are evaluated per row and per field. A list of 100 records showing 30 columns is potentially thousands of evaluations for one page load.
If those ACLs are declarative, that is fast. If they are script-based, you are executing thousands of small scripts, and if one of those scripts does a GlideRecord query, you have reinvented problem number one in a place where it is much harder to see.
Script ACLs that query other tables are worth hunting for specifically. Where you need them, cache the result in the session or scratchpad rather than re-querying per row.
7. The client side
Some slowness is not server-side at all:
- Many client scripts on one form, each doing work on load.
- Widgets making several
GlideAjaxcalls serially when one server call could return everything. g_formmanipulation in loops, forcing repeated re-render.- Dot-walked reference fields as list columns, each adding a join to the list query.
The tell is a form that is slow to become usable while the server-side transaction log shows a fast response.
How to actually find yours
Do not start optimising from the list above. Start by measuring, in this order.
1. Find the slow transactions. The transaction log tells you which specific operations are slow, for whom, and how often. Sort by duration, then look at what appears repeatedly — a 30-second report someone runs monthly matters far less than a 3-second form load happening ten thousand times a day.
2. Reproduce with session debug and SQL tracing on. This is the step that ends the guessing. It shows the actual queries, their count, and their timing for a single transaction. Query count is often more informative than total duration — three hundred queries where you expected three tells you exactly which loop to look at, immediately.
3. Check the slow query log. Recurring expensive queries against large tables point straight at missing indexes and CONTAINS conditions.
4. Only then change something. And re-measure after, because the fix that obviously should have helped sometimes doesn’t, and the reason is usually that you fixed the second-slowest thing.
The uncomfortable pattern
Almost everything above is a consequence of code that was correct on a small dataset and never revisited as the data grew. The loop was fine at forty records. The unindexed condition was fine before that table had two million rows. Nothing broke; the instance just got slower every quarter until somebody complained.
The practical defence is to make data volume a first-class consideration when reviewing platform code — ask “what does this do at ten times current volume” the way you would ask it of any system that has to keep working as it grows. That question at review time is cheaper than any of the diagnostics above.
We do this as remediation work fairly often within our ServiceNow practice, and the outcome is consistent: two or three specific fixes account for most of the improvement, and finding them takes hours rather than weeks once you are measuring instead of guessing. If your instance has been getting steadily slower and nobody can point at why, that is a well-bounded engagement with a fairly predictable answer.