ServiceNow GlideRecord Best Practices: Pagination, Performance, and Production Patterns
ServiceNow GlideRecord Best Practices: Pagination, Performance, and Production Patterns
GlideRecord is the backbone of every ServiceNow developer's toolkit. It's the API you reach for whether you're querying incidents, updating configuration items, or building complex automation logic. But here's the uncomfortable truth: most ServiceNow scripts abuse GlideRecord in ways that quietly tank instance performance.
I've seen instances brought to their knees by a single business rule doing nested GlideRecord loops on the incident table. I've seen homepage load times spike because a client script was doing synchronous GlideRecord queries on every form load. And I've seen developers write innocent-looking scripts that consumed gigabytes of memory.
The good news: these are all avoidable. This guide covers the patterns that matter — from basic querying to production-grade pagination and performance optimization.
How GlideRecord Actually Works
Before diving into patterns, let's be clear on how GlideRecord behaves, because misunderstandings here cause most of the performance problems I see in the wild.
When you call myRecord.query() on a GlideRecord:
- The query is sent to the database
- The result set is prepared on the server — but no rows are fetched yet
- Rows are fetched one at a time as you call
myRecord.next()inside a while loop - Each
next()call triggers a database round-trip
This is why a loop without a row limit is so dangerous — you're potentially making hundreds or thousands of individual database round-trips inside a single script execution.
The Cardinal Rule: Always Set a Row Limit
This should be obvious, but I'll say it anyway because I still see it violated in production code:
// BAD: No limit — will fetch everything
function badExample() {
var gr = new GlideRecord('incident');
gr.addQuery('state', '!=', 7);
gr.query();
while (gr.next()) {
gs.info(gr.number.toString());
}
}
// GOOD: Explicit limit
function goodExample() {
var gr = new GlideRecord('incident');
gr.addQuery('state', '!=', 7);
gr.setLimit(1000); // Cap at 1000 rows
gr.query();
while (gr.next()) {
gs.info(gr.number.toString());
}
}
Set your row limit before calling query() — ideally based on what you actually need. If you only need one record, use get() instead.
Use get() When You Need a Single Record
If you're fetching by sys_id or a unique field, don't use query() + next(). Use get():
// BAD: query() + next() for a single record
var gr = new GlideRecord('incident');
gr.addQuery('sys_id', 'abc123');
gr.query();
if (gr.next()) {
// got it
}
// GOOD: get() is cleaner and faster
var gr = new GlideRecord('incident');
if (gr.get('abc123')) {
// gr is now the record you wanted
gs.info(gr.number);
}
get() returns true if the record was found and automatically locks the row for update if you call gr.update(). It also handles the case where the passed value is already a GlideElement or a string sys_id.
Field Selection: Query Only What You Need
By default, GlideRecord fetches every column. When you only need a few fields, specify them:
var gr = new GlideRecord('incident');
gr.addQuery('assigned_to', gs.getUserID());
gr.addQuery('state', 2);
gr.selectFields(['number', 'short_description', 'opened_at', 'state']);
gr.query();
while (gr.next()) {
// Only the selected fields are loaded
var num = gr.getValue('number');
var desc = gr.getValue('short_description');
}
This reduces memory footprint and query execution time, especially on tables with dozens of columns or large text/blob fields.
Pagination: The Right Way
Real pagination in GlideRecord isn't LIMIT/OFFSET the way SQL does it. ServiceNow's approach is cursor-based — you track your position through the result set using the last seen sys_id. Here's the production-ready pattern:
function paginateIncidents(pageSize, offset) {
pageSize = pageSize || 20;
var gr = new GlideRecord('incident');
gr.addQuery('active', 'true');
gr.orderBy('sys_created_on');
gr.chooseWindow(offset, offset + pageSize - 1, true);
gr.query();
var results = [];
while (gr.next()) {
results.push({
sys_id: gr.getUniqueValue(),
number: gr.getValue('number'),
short_description: gr.getValue('short_description'),
state: gr.getDisplayValue('state')
});
}
return results;
}
The chooseWindow(start, end, includeBoundary) method is the key — it handles offset-based pagination without the performance problems of naive LIMIT/OFFSET in raw SQL.
For Batch Processing: Iterate with Boundaries
When you need to process millions of records (think data migration or bulk updates), batch using sys_id ranges:
function processInBatches(batchSize, lastId) {
batchSize = batchSize || 500;
var gr = new GlideRecord('incident');
if (lastId) {
gr.addQuery('sys_id', '>', lastId);
}
gr.addQuery('state', '!=', 7);
gr.orderBy('sys_id');
gr.setLimit(batchSize);
gr.query();
var processed = 0;
var lastProcessedId = null;
while (gr.next()) {
// Your business logic here
gs.info('Processing: ' + gr.number);
lastProcessedId = gr.getUniqueValue();
processed++;
}
return {
processed: processed,
lastId: lastProcessedId,
hasMore: processed === batchSize
};
}
Call this in a loop from a scheduled script, checking hasMore each iteration. This pattern is safe for tables with billions of rows and won't blow up your transaction memory.
Performance Anti-Patterns to Avoid
Anti-Pattern 1: Nested GlideRecord Loops
// TERRIBLE performance — O(n²) complexity
var incidents = new GlideRecord('incident');
incidents.addQuery('assigned_to', current.user_id);
incidents.query();
while (incidents.next()) {
var gr = new GlideRecord('task'); // Inner query runs per incident!
gr.addQuery('parent', incidents.sys_id);
gr.query();
while (gr.next()) {
gs.info(gr.number);
}
}
Instead, use a single query with a JOIN or aggregate, or use GlideAggregate to count first.
Anti-Pattern 2: Synchronous GlideRecord in Client Scripts
Never do this in a Client Script or UI Policy:
// This will freeze the browser
function onLoad() {
var ga = new GlideAjax('MyScriptInclude');
ga.addParam('sysparm_name', 'getOpenCount');
ga.get(); // async
// Never synchronously wait for this
}
Always use GlideAjax (asynchronous) from client scripts. Never use new GlideRecord() in a Client Script — it runs server-side but blocks the browser thread.
Anti-Pattern 3: Querying Without Indexed Fields
If your addQuery() doesn't use an indexed field, ServiceNow has to table-scan. Common culprit:
// short_description is often not indexed
gr.addQuery('short_description', 'CONTAINS', searchTerm);
// Use the indexed field instead (number, sys_id, or a proper reference)
gr.addQuery('number', 'CONTAINS', searchTerm);
Check indexed fields in Table Administration → the "Indexes" tab.
When to Use GlideAggregate Instead
GlideAggregate runs SQL aggregation functions at the database level. It's dramatically more efficient than looping with GlideRecord when you just need counts or sums:
// BAD: Loop through potentially thousands of records just to count
var count = 0;
var gr = new GlideRecord('incident');
gr.addQuery('state', 2);
gr.query();
while (gr.next()) {
count++;
}
// GOOD: Let the database do the counting
var ga = new GlideAggregate('incident');
ga.addQuery('state', 2);
ga.addAggregate('COUNT');
ga.query();
if (ga.next()) {
var count = ga.getAggregate('COUNT');
}
GlideAggregate supports: COUNT, SUM, MIN, MAX, AVG. You can also group by fields with groupBy('field').
Quick Reference: Method Decision Guide
| Scenario | Method | Why |
|---|---|---|
| Fetch single record by sys_id | get() | Single DB round-trip, cleaner syntax |
| Fetch few records with known filter | query() + setLimit() | Controlled result set |
| Paginate through large dataset | chooseWindow() or cursor-based | Memory-efficient, reproducible |
| Count records | GlideAggregate | DB-level aggregation |
| Sum/avg/min/max | GlideAggregate | DB-level aggregation |
| Check if records exist | hasNext() after query | Don't iterate just to check existence |
A Note on setWorkflow(false)
When you're updating or inserting records inside loops — especially in scheduled background jobs — disable workflow:
var gr = new GlideRecord('incident');
gr.addQuery('active', 'true');
gr.setLimit(100);
gr.query();
while (gr.next()) {
gr.setWorkflow(false); // Skip workflow engines for bulk operations
gr.state = 7; // Resolved
gr.update();
}
This prevents workflow triggers, approvals, and notifications from firing on each update. Use it deliberately — only when you're sure you don't need those features in your bulk operation.
Wrapping Up
GlideRecord is deceptively simple, which is exactly why it causes so many performance problems. The fix is almost always the same: set a limit, fetch only what you need, and pick the right method for the job.
Bookmark this one. You'll want to come back to the pagination patterns when you're processing that migration script at 2 AM.
