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.

->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? 

  • 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

2 Comments
aasch
Mega Sage

Hi,

as far as I know, GlideQuery is a wrapper over GlideRecord. The inventor of GlideQuery explicitly states this here ("As a wrapper over GlideRecord [...]").

If that's the case, you shouldn't see any performance gain by selecting "individual fields", because under the hood GlideQuery still returns all the fields of a record, as GlideRecord does.

 

Can you corroborate this?

 

The official documentation (Zurich) also notes the following:

"Note: Because the GlideQuery script include converts GlideRecord objects into standard JavaScript objects, it may take longer to execute queries. To reduce performance issues, avoid creating loops that iterate over large numbers of records."

 

I take that to mean that GlideQuery does not improve performance per se and that performance even potentially worsens with a greater result set - contrary to what "selecting" individual fields would suggest.
It just doesn't seem to be handed down to SQL.

Ratnakar7
Mega Sage

Hi @aasch ,

Thanks for reading! The idea behind this blog is to go beyond "best practices" and think about how developers can design with both time (when things happen, sequencing, latency) and space (where things run, context, boundaries) in mind. ServiceNow gives us strong guardrails, but the point is to encourage developers to step back and see the bigger picture - how choices about timing and placement affect scalability, maintainability, and user experience. It's less about replacing existing practices and more about expanding the way we frame problems.

 

You're absolutely right that GlideQuery is a wrapper over GlideRecord, so it doesn't magically change the way records are fetched under the hood. My intention with the blog was aligned with what the ServiceNow documentation itself "Note: .. avoid creating loops ..." (like nested loops or the brute‑force "Approach A" I showed) that can hurt performance.

That's why I highlighted GlideQuery with set comparison (using array utilities). It lets you directly point to the exact need - for example, adding or removing group members - in a way that's more readable and optimized compared to manual looping. So while GlideQuery isn't about raw SQL performance gains, it does help write cleaner, safer, and more maintainable code for these scenarios.

 

Thanks,
Ratnakar