Skip to Content
  1. Home
  2. /
  3. Blog
  4. /
  5. ServiceNow GlideRecord Best Practices: Pagination, Performance, and Production Patterns
Monday, May 4, 2026

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:

  1. The query is sent to the database
  2. The result set is prepared on the server — but no rows are fetched yet
  3. Rows are fetched one at a time as you call myRecord.next() inside a while loop
  4. 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:

JavaScript
// 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():

JavaScript
// 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:

JavaScript
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:

JavaScript
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:

JavaScript
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

JavaScript
// 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:

JavaScript
// 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:

JavaScript
// 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:

JavaScript
// 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

ScenarioMethodWhy
Fetch single record by sys_idget()Single DB round-trip, cleaner syntax
Fetch few records with known filterquery() + setLimit()Controlled result set
Paginate through large datasetchooseWindow() or cursor-basedMemory-efficient, reproducible
Count recordsGlideAggregateDB-level aggregation
Sum/avg/min/maxGlideAggregateDB-level aggregation
Check if records existhasNext() after queryDon'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:

JavaScript
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.

Was this article helpful?