Ratnakar7
Mega Sage

Improving ServiceNow Performance with Time & Space Complexity Considerations.

We often emphasize coding best practices: reusable Script Includes, Scoped App, ACLs, scalable design. Yet one critical dimension is overlooked - algorithmic efficiency

Writing brute-force code may fulfil requirements quickly, but it can cripple performance as data grows. Inefficient scripts don't just slow one user; they consume semaphore nodes, leading to instance-wide latency or outages

Scaling an instance isn't just about Script Includes or Scoped App; it's about Computational Efficiency.
In this post, we'll move beyond basic coding standards and dive into the world of Big O Notation, JavaScript Object, and Functional Programming to ensure your instance remains lightning-fast.

When we write a nested loop to compare two datasets, we aren't just writing "bad code" - we are creating a  O(n^2) time-bomb. As your CMDB grows from 1,000 to 100,000 records, that script doesn't just get a little slower; it becomes an exponential burden that can freeze an entire application node.


You've likely seen the dreaded error:

“Transaction Cancelled: maximum execution time exceeded.”

 

This happens when scripts run beyond the 5-minute quota, often due to nested loops or excessive GlideRecord queries.

Scenario: The "Audit & Update"

The Requirement: We have an external staging table with 5,000 "Server Status" updates. We need to find the corresponding CI in the cmdb_ci_server table (which has 150,000 records) and update the "Last Checked" date.

 

-> Approach A: The Brute Force (Nested GlideRecord Queries):

This approach is "logical" but architecturally disastrous. It uses a nested query pattern.

// O(N * M) Complexity - The "Transaction timeout"
var staging = new GlideRecord('u_server_updates');
staging.query();

while (staging.next()) {
    // For EVERY staging record, we hit the database again
    var ci = new GlideRecord('cmdb_ci_server');
    ci.addQuery('serial_number', staging.u_serial); 
    ci.query(); 
    
    if (ci.next()) {
        ci.u_last_checked = new GlideDateTime();
        ci.update();
    }
}


The Problem: If there are 5,000 staging rows, this script performs 5,001 database queries. The database "handshake" overhead alone will likely exceed the transaction limit.

Complexity: O(n*m)


-> Approach B: The Optimized Solution (JavaScript Object Pattern)

Here, we apply a Space-Time Trade-off. We use a little more memory (Space) to achieve a massive gain in speed (Time).

// O(N + M) Complexity - The "Scalable" Approach
var serverMap = {};
var serials = [];

// 1. Collect all serials first - O(N)
var staging = new GlideRecord('u_server_updates');
staging.query();
while (staging.next()) {
    var s = staging.getValue('u_serial');
    serials.push(s);
    serverMap[s] = true; // Use a JavaScript Object for O(1) lookups later
}

// 2. Perform ONE bulk query - O(M)
var ci = new GlideRecord('cmdb_ci_server');
ci.addQuery('serial_number', 'IN', serials);
ci.query();

while (ci.next()) {
    // 3. Constant time lookup
    if (serverMap[ci.serial_number]) {
        ci.u_last_checked = new GlideDateTime();
        ci.setWorkflow(false); // Optimization: Skip Business Rules
        ci.update();
    }
}

The Result: We reduced 5,000 queries to exactly

Complexity: O(n + m) This script will finish in seconds, not minutes.

 

-> Level 3: The Modern Architect - Functional Programming & Memory Management

While Hash Maps solve the Time problem, we look at the next horizon: Resource Utilization (Space Complexity). Traditional GlideRecord is "heavy." It fetches every single column in a row. If a table has 100 columns and you only need the sys_id, you are wasting 99% of your memory footprint. GlideQuery solves this through field selection and streaming.


Advanced Pattern: The "Set Comparison":

The Scenario: You are syncing a list of 20,000 Group Memberships from an external IAM system (like Saviynt) to ServiceNow.

  • The Problem: You need to find which users need to be added and which need to be removed.
  • The Inefficient Way: Loop through the external list and check the DB for each user (O(n * m)).
  1. The "Functional" Optimized Approach (GlideQuery + ArrayUtil)

Instead of manual loops, we use Set Theory. We treat the ServiceNow data and the External data as two sets and find the Difference.

// 1. Get current SN Members as an Array - O(m)
var snMembers = new GlideQuery('sys_user_grmember')
    .where('group', 'sys_id_of_group')
    .select('user') // Only fetch the user field (Memory optimization)
    .map(function (row) { return row.user; })
    .toArray(20000);

var externalMembers = ['sys_id_1', 'sys_id_2', ...]; // From your integration

// 2. Use ArrayUtil for high-speed Set comparison
var au = new ArrayUtil();

// Users in External but NOT in SN (To be ADDED)
var toAdd = au.diff(externalMembers, snMembers); 

// Users in SN but NOT in External (To be REMOVED)
var toRemove = au.diff(snMembers, externalMembers);

// 3. Bulk Action
// This is much faster because we aren't "guessing" what to update
toAdd.forEach(function(userId) {
    new GlideQuery('sys_user_grmember')
        .insert({
            user: userId,
            group: 'sys_id_of_group'
        });
});
toRemove.forEach(function(userId) {
    new GlideQuery('sys_user_grmember')
        .where('user', userId)
        .where('group', 'sys_id_of_group')
        .deleteMultiple();
});


The "Resource Utilization" Deep Dive

When we talk about Space Complexity, we are talking about Heap Memory.

  • GlideRecord: When you run while(gr.next()), ServiceNow keeps the record pointer in memory. If you query 50,000 records, you are putting pressure on the JVM (Java Virtual Machine) heap.
  • GlideQuery .select(): It only retrieves the fields you specify. If a table has 100 fields but you only need the sys_id, select('sys_id') reduces the memory footprint by 90%+.

----------------------------------------------------------------------------------------------------------------------------------------------------

->Best Practices for Performance:

Minimize GlideRecord queries: Use addQuery, addExtraFields, and bulk queries.

Avoid nested loops: Replace with hash maps or sets.

Index fields: Query on indexed fields to avoid full table scans.

Batch processing with async mechanism: Use Script Action along with eventScheduler  or scheduled jobs for large datasets.

Measure complexity: Think in terms of Big O notation before coding.

 

Verdict

Efficiency isn't a "nice to have" - it's a requirement for enterprise stability. If you aren't thinking about how your script will behave when the table hits 1 million rows, you aren't building for the future: you're just building technical debt.

 

As ServiceNow professionals, our job isn't just to make the "Submit" button work. Our job is to ensure the system remains fast as the company grows.

Next time you write a script, ask yourself:

  • Is there a query inside this loop? (Target: 0)

  • Am I fetching fields I don't need? (Use GlideQuery)

  • Can I use a JavaScript Object or Set Difference instead of a nested loop?

Performance isn't a feature you add at the end; it’s a mindset you apply at the start.



Thanks,
Ratnakar