Coding Best Practice Guidelines - Memory Focus<!-- /*NS Branding Styles*/ --> .ns-kb-css-body-editor-container { p { font-size: 12pt; font-family: Lato; color: var(--now-color--text-primary, #000000); } span { font-size: 12pt; font-family: Lato; color: var(--now-color--text-primary, #000000); } h2 { font-size: 24pt; font-family: Lato; color: var(--now-color--text-primary, black); } h3 { font-size: 18pt; font-family: Lato; color: var(--now-color--text-primary, black); } h4 { font-size: 14pt; font-family: Lato; color: var(--now-color--text-primary, black); } a { font-size: 12pt; font-family: Lato; color: var(--now-color--link-primary, #00718F); } a:hover { font-size: 12pt; color: var(--now-color--link-primary, #024F69); } a:target { font-size: 12pt; color: var(--now-color--link-primary, #032D42); } a:visited { font-size: 12pt; color: var(--now-color--link-primary, #00718f); } ul { font-size: 12pt; font-family: Lato; } li { font-size: 12pt; font-family: Lato; } img { display: ; max-width: ; width: ; height: ; } } Introduction Memory management is critical in ServiceNow development. Poor memory practices can lead to transaction failures, platform slowdowns, and system instability affecting all users. Unlike traditional applications where memory issues might only impact a single user, ServiceNow's shared infrastructure means that memory problems in one script can cascade across the entire instance. Memory issues in ServiceNow typically manifest as general slowness of the instance, transaction timeouts, Maximum execution time exceeded errors, or complete transaction rollbacks. These problems often stem from developers inadvertently holding references to large objects, caching excessively, or processing unlimited datasets without proper controls. This knowledge base article focuses specifically on coding practices that prevent memory-related issues, ensuring your ServiceNow applications run efficiently and reliably at scale. Coding Best Practices Validate Variables Before Using Them in GlideRecord Queries Passing null or undefined values into a GlideRecord query is one of the most dangerous and common mistakes in ServiceNow development. Unlike most programming errors that fail loudly, this one fails silently — the script runs without error, but the query executes against far more data than intended, loading unexpected result sets into memory and potentially corrupting or deleting records There are two distinct failure modes, each with different consequences: addQuery(fieldName, undefinedValue) — the platform treats the undefined value as NULL and returns all records where that field is null. Depending on the table and field, this could be thousands of records.addQuery(undefinedFieldName, value) — the platform cannot match the field name and returns the entire table. On a large table like task, cmdb_ci, or sys_user, this can load millions of records into memory. Both scenarios can trigger transaction timeouts, heap exhaustion, and in the case of Business Rules or Fix Scripts, unintended mass updates or deletions. Why This Matters: Undefined query conditions produce result sets orders of magnitude larger than intendedEach unexpected record retrieved allocates memory — a query that should return 1 record returning 50,000 consumes 50,000× the expected heapThe script produces no error and may appear to work correctly in development where tables are small, only failing under production data volumesIn write contexts (Business Rules, Fix Scripts), an unexpected full-table result set means every record in scope gets updated or processed — not just the intended onesDeeply dot-walked values that resolve to nil are a frequent and easy-to-miss source of undefined conditions Best Practices: Always validate that variables have a value before passing them into addQuery, addEncodedQuery, or GlideRecord.get()Use GlideElement.nil() to test reference fields and dot-walked values before using them as query conditionsUse JSUtil.notNil() as a general-purpose nil check for any variable typeEnable the system property glide.invalid_query.returns_no_rows = true — this causes an invalid field name in addQuery to return zero results (via WHERE 1=0) instead of the full table, converting a catastrophic failure into a recoverable oneBe especially vigilant when query values come from function parameters, GlideRecord fields, system properties, or REST request payloads — all of which can be null or undefined Example — undefined value passed as a query condition: // MEMORY ISSUE - userId may be undefined if the caller_id field is empty. // addQuery treats undefined as NULL, returning all incidents // where caller_id IS NULL — potentially thousands of records. function processIncidentsForUser(userId) { var gr = new GlideRecord('incident'); gr.addQuery('caller_id', userId); gr.query(); while (gr.next()) { processIncident(gr); } } // CORRECT - Validate before querying function processIncidentsForUser(userId) { if (!userId) { gs.warn('processIncidentsForUser: userId is null or undefined, aborting'); return; } var gr = new GlideRecord('incident'); gr.addQuery('caller_id', userId); gr.query(); while (gr.next()) { processIncident(gr); } } Example — nil dot-walked value used as a downstream query condition: // MEMORY ISSUE - If assigned_to is empty on the incident, the dot-walked sys_id // resolves to nil. Passing nil into the next query returns all tasks // where assigned_to IS NULL. var inc = new GlideRecord('incident'); inc.get(incidentSysId); var assignedTo = inc.assigned_to.sys_id; // nil if assigned_to is empty var tasks = new GlideRecord('task'); tasks.addQuery('assigned_to', assignedTo); // unintended large result set tasks.query(); // CORRECT - Check GlideElement.nil() before using a dot-walked value as a condition var inc = new GlideRecord('incident'); inc.get(incidentSysId); if (inc.assigned_to.nil()) { gs.warn('Incident ' + incidentSysId + ' has no assignee — skipping task query'); return; } var assignedTo = inc.getValue('assigned_to'); // extract primitive after nil check var tasks = new GlideRecord('task'); tasks.addQuery('assigned_to', assignedTo); tasks.query(); Example — undefined field name causes a full table scan: // MEMORY ISSUE - If getTargetField() returns undefined, the platform cannot // match the field name and returns the entire table. No error // is thrown; the script continues processing millions of records. var fieldName = getTargetField(); // returns undefined if configuration is missing var gr = new GlideRecord('cmdb_ci'); gr.addQuery(fieldName, targetValue); // full table scan gr.query(); // CORRECT - Validate the field name before constructing the query var fieldName = getTargetField(); if (!fieldName) { gs.error('getTargetField returned null — cannot build query, aborting'); return; } var gr = new GlideRecord('cmdb_ci'); gr.addQuery(fieldName, targetValue); gr.query(); System property safety net: Setting glide.invalid_query.returns_no_rows to true changes the behavior when an undefined or invalid field name is passed to addQuery. Instead of returning the full table, the platform rewrites the query as WHERE 1=0 and returns zero results instantly. This should be enabled on all instances including sub-production. It does not replace input validation — a query that silently returns nothing instead of the intended records can still cause incorrect behavior downstream — but it eliminates the risk of a full table scan from a missing field name. Never Store GlideRecord Objects - Extract Primitive Values One of the most common causes of memory issues in ServiceNow is storing entire GlideRecord objects instead of extracting their primitive values. GlideRecord objects maintain references to database connections, metadata, and internal state that consume significant memory. Why This Matters: GlideRecord objects are heavyweight and retain unnecessary overheadStoring them in arrays or collections prevents garbage collectionMemory accumulates quickly when processing multiple recordsCan lead to "JavaScript evaluation error" or transaction timeouts Best Practices: Always use getValue() to extract string valuesUse getDisplayValue() for reference fields and choice fieldsNever push entire GlideRecord objects into arraysExtract only the fields you actually need Example: // MEMORY ISSUE - Storing GlideRecord objects var incidents = []; var gr = new GlideRecord('incident'); gr.addQuery('active', true); gr.query(); while (gr.next()) { incidents.push(gr); // Stores entire object with all metadata } // CORRECT - Extracting primitive values var incidents = []; var gr = new GlideRecord('incident'); gr.addQuery('active', true); gr.query(); while (gr.next()) { incidents.push({ sys_id: gr.getValue('sys_id'), number: gr.getValue('number'), short_description: gr.getValue('short_description'), priority: gr.getValue('priority') }); // Only stores string primitives } Stringify Dot-Walked Fields Immediately Dot-walking to reference fields creates object references that persist in memory (they are usually stored as GlideElement references in memory). These references maintain connections to related tables and can exponentially increase memory consumption. Why This Matters: Dot-walked references maintain database connectionsMemory grows with the depth and breadth of relationshipsCan cause unexpected memory retention even after processing Best Practices: Always call toString(), getValue(), or getDisplayValue() on dot-walked fieldsNever store dot-walked references directlyBe explicit about converting references to strings Example: // MEMORY ISSUE - Storing dot-walked references var incidentData = []; var gr = new GlideRecord('incident'); gr.query(); while (gr.next()) { var data = { number: gr.getValue('number'), assigned_to: gr.assigned_to, // Object reference maintained caller: gr.caller_id, // Object reference maintained assignment_group: gr.assignment_group // Object reference maintained }; incidentData.push(data); } // CORRECT - Stringifying dot-walked fields var incidentData = []; var gr = new GlideRecord('incident'); gr.query(); while (gr.next()) { var data = { number: gr.getValue('number'), assigned_to: gr.assigned_to.getDisplayValue(), // String value assigned_to_id: gr.getValue('assigned_to'), // String sys_id caller: gr.caller_id.getDisplayValue(), // String value assignment_group: gr.assignment_group.getDisplayValue() // String value }; incidentData.push(data); } String Concatenation in Loops In JavaScript, strings are immutable. Every time you use the + operator to concatenate two strings, the engine allocates a brand-new string in memory containing the combined content, and the old string becomes eligible for garbage collection. In a tight loop, this means you are not accumulating one string — you are allocating a new, progressively larger string on every iteration. For a loop that runs N times producing a final string of length L, the total memory allocated across all iterations is proportional to N² × L. For small loops this is negligible. For loops processing hundreds or thousands of records — common in ServiceNow report builders, integration payloads, and email generators — this becomes a significant source of heap pressure and slowdown. Why This Matters: Each += in a loop allocates a new string equal in size to everything accumulated so far, plus the new fragmentMemory consumption grows quadratically, not linearly, with the number of iterationsIntermediate strings that are immediately discarded still put pressure on the garbage collectorLong string-building operations can push transactions toward the memory limit before any meaningful processing is completeServiceNow's Rhino JS engine does not support ES6 template literals, making Array.join() the correct idiomatic alternative Best Practices: Never use += to build a string inside a loop — use an array and call .join() once at the endPush each fragment into the array; joining is a single O(N) allocation at the endFor multi-part strings with separators (CSV rows, XML elements, newline-separated lines), pass the separator to .join()If building structured output (JSON, XML), prefer dedicated serializers (JSON.stringify(), XMLDocument) over manual string construction Example: // MEMORY ISSUE - String concatenation in a loop var html = ''; var gr = new GlideRecord('incident'); gr.addQuery('active', true); gr.setLimit(500); gr.query(); while (gr.next()) { html += '<tr>'; // new string allocated html += '<td>' + gr.getValue('number') + '</td>'; // new string allocated html += '<td>' + gr.getValue('short_description') + '</td>'; // new string allocated html += '</tr>'; // new string allocated } // By the end, hundreds of intermediate strings were allocated and discarded // CORRECT - Building an array of parts, joining once var parts = []; var gr = new GlideRecord('incident'); gr.addQuery('active', true); gr.setLimit(500); gr.query(); while (gr.next()) { parts.push('<tr>'); parts.push('<td>' + gr.getValue('number') + '</td>'); parts.push('<td>' + gr.getValue('short_description') + '</td>'); parts.push('</tr>'); } var html = parts.join(''); // single allocation of the final string The corrected version pushes small, fixed-size string literals and field values into an array on each iteration — a cheap operation with no large allocations. The single .join('') call at the end allocates the final string exactly once. Batch Processing In ServiceNow server-side JavaScript (Background Scripts, Script Includes, Scheduled Jobs, Fix Scripts, Business Rules), it’s easy to accidentally load thousands of records (or large strings/arrays derived from them) into memory. That increases heap usage, slows execution, and raises the risk of long-running transactions or node instability. The safest approach is to iterate records and commit work incrementally, keeping only a small working set in memory at any time. Why this matters: Loading entire result sets into memory before processing can exhaust heap on tables with thousands of recordsTwo-pass patterns (collect IDs, then loop) double the memory footprint and the workLong-running loops with no checkpointing cannot resume after failure, forcing a restart from scratch Best Practices: Never build huge arrays of Sys IDs/GlideRecords just to loop later. Prefer a single pass over a GlideRecord result setProcess and discard: for each record, do the work, update, and move on—avoid keeping per-record payloads in memoryUse pagination/chunking when each record’s processing is heavy or you need periodic checkpoints: order by a stable field (often sys_id), keep a “last processed” marker, and loop in batchesLimit what you query: add selective conditions, use setLimit() for batches, and avoid dot-walking into large text fields unless requiredPrefer aggregate queries (e.g., GlideAggregate) when you only need counts/min/max instead of full rowYield checkpoints: log progress per batch; for long jobs, consider splitting work into multiple scheduled runs (checkpoint stored in a system property/table)Be careful with display values: getDisplayValue() can be expensive—use only when needed.Avoid unnecessary serialization (e.g., converting records to JSON strings) during processing. Example (Before): var ids = []; var gr = new GlideRecord('incident'); gr.addQuery('active', true); gr.query(); while (gr.next()) { ids.push(gr.getUniqueValue()); } for (var i = 0; i < ids.length; i++) { var inc = new GlideRecord('incident'); if (inc.get(ids[i])) { inc.u_processed = true; inc.update(); } } The problem in the above code is that the code reads the whole result set into an array first, doubling the work that needs to be done and keeping all "Sys ID" [sys_id] values in memory at once. Example (After): var gr = new GlideRecord('incident'); gr.addQuery('active', true); gr.query(); while (gr.next()) { gr.u_processed = true; gr.update(); } The above code is improved as we process each record once per iteration, keeping memory usage roughly constant. Better Example (After): var BATCH_SIZE = 500; var lastSysId = gs.getProperty('x_myapp.incident.last_sys_id', ''); while (true) { var batch = new GlideRecord('incident'); batch.addQuery('active', true); if (lastSysId) { batch.addQuery('sys_id', '>', lastSysId); // relies on stable ordering } batch.orderBy('sys_id'); batch.setLimit(BATCH_SIZE); batch.query(); var count = 0; while (batch.next()) { // Do work batch.u_processed = true; batch.update(); lastSysId = batch.getUniqueValue(); count++; } if (count === 0) { break; // done } // Persist checkpoint after each batch gs.setProperty('x_myapp.incident.last_sys_id', lastSysId); gs.info('Processed batch of ' + count + ' incidents; last sys_id=' + lastSysId); } The above code is even further improved as we can use a stable ordering method (eg., sys_id) and a stored checkpoint to batch the work into smaller chunks. Checkpointing: store progress in a system property (with Ignore Cache set to true) or in a small custom table so the job can safely resume after failure Ordering: use a stable, indexed field for batching. The sys_id field is commonly used, or a date field combined with sys_id. Always pair the sys_id > last condition with orderBy('sys_id') Updates inside loops: keep the update payload small. If you’re doing complex logic per record, consider moving it into a Script Include and keeping the loop “thin.” Transaction size: large update loops can still be heavy; batching allows progress logging and restartability Don’t overuse GlideRecord objects: avoid creating a new GlideRecord and doing get() per Sys ID when you already have the record in-hand from the query Reduce query scope: only fetch what you need (conditions first), and avoid touching large fields unless required. Use setLimit() to Control Record Processing Processing unlimited records is one of the fastest ways to exhaust memory. Every record retrieved consumes memory, and without limits, even moderate-sized tables can cause memory exhaustion. Why This Matters: Each GlideRecord retrieval allocates memoryLarge result sets can exceed transaction memory limitsMemory pressure increases with concurrent transactionsCan cause cascading failures affecting other users Best Practices: Always use setLimit() when you don't need all recordsSet realistic limits based on actual requirements (typically 100-1000)Use getRowCount() (or better yet, use GlideAggregate to check record counts) before processing to assess dataset sizeImplement pagination for large datasets Example: // MEMORY ISSUE - No limit on records processed var gr = new GlideRecord('incident'); gr.addQuery('state', '1'); gr.query(); // Could retrieve thousands of records var count = 0; while (gr.next()) { processIncident(gr); count++; } // CORRECT - Using setLimit() var LIMIT = 500; var gr = new GlideRecord('incident'); gr.addQuery('state', '1'); gr.setLimit(LIMIT); gr.query(); var count = 0; while (gr.next()) { processIncident(gr); count++; } gs.info('Processed ' + count + ' incidents (limit: ' + LIMIT + ')'); // BETTER - Check count first, then process in batches var counter = new GlideAggregate('incident'); counter.addQuery('state', '1'); counter.addAggregate('COUNT'); counter.query(); var totalCount = 0; if (counter.next()) { totalCount = parseInt(counter.getAggregate('COUNT')); } if (totalCount > 1000) { gs.warn('Large dataset detected (' + totalCount + ' records). Consider batching.'); // Process in batches via Scheduled Job } else { // Process directly with limit processRecords(totalCount); } Avoid Recursion Every thread maintains a call stack. Each function call creates a stack frame that is pushed onto the call stack in a LIFO (last-in, first-out) manner. Each stack frame contains local variables that are allocated within that function. As long as the frame is on the stack, the memory allocated for those local variables are strongly referenced, and will not be reclaimed by garbage collection. Allocated memory is only released when the function returns and the frame is popped off the stack. When calling a function recursively, each recursive call pushes a new stack frame onto the stack, which continues to grow until the recursion ends and functions are popped off the stack. If the recursion has no sane exit condition, then the stack itself can grow excessively and the local variables can cause high memory usage. For example: // MEMORY ISSUE - Avoid Recursion _collectChildren: function(parentSysId, results) { var gr = new GlideRecord('incident'); gr.addQuery('parent_incident', parentSysId); gr.query(); while (gr.next()) { results.push({ sys_id: gr.getUniqueValue(), number: gr.getValue('number'), short_description: gr.getValue('short_description'), state: gr.getDisplayValue('state') }); // Recursive call this._collectChildren(gr.getUniqueValue(), results); } }, The above function is a common example of a depth-first traversal of a hierarchical relationship. It demonstrates several of the pitfalls recursion can cause. Stack Frame Accumulation: Each recursive call adds a stack frame containing variables such as "parentSysId", "results" and "gr". In a worst-case scenario, where the hierarchy is very deep (eg., 1000 levels), you get over 1000 stack frames, each holding a GlideRecord object. This can trigger a Stack overflow. GlideRecord Object Retention: A GlideRecord object is quite large; it contains metadata and data. Having 1000 GlideRecord objects can consume a lot of memory for the thread. Memory Growth Through Results Array: The results array is passed by reference across all frames. As the traversal descends, objects accumulate in that shared array — but the full depth of the call stack must unwind before any GlideRecord in any frame can be garbage-collected. Peak memory is the sum of all GlideRecord objects across all open frames simultaneously. Best Practices: Prefer iterative approaches using an explicit stack (array) or queue over recursive callsIf recursion is genuinely needed, ensure a clear, provably-reachable exit condition and enforce a hard depth limitAvoid holding GlideRecord objects across recursive calls — extract primitives before recursingFor hierarchical traversals, use GlideRecord with a work queue rather than call-stack recursion Example (corrected — iterative depth-first traversal): // CORRECT - Iterative approach using an explicit queue _collectChildren: function(rootSysId) { var results = []; var queue = [rootSysId]; // explicit work queue instead of call stack while (queue.length > 0) { var parentSysId = queue.shift(); var gr = new GlideRecord('incident'); gr.addQuery('parent_incident', parentSysId); gr.query(); while (gr.next()) { var childSysId = gr.getUniqueValue(); results.push({ sys_id: childSysId, number: gr.getValue('number'), short_description: gr.getValue('short_description'), state: gr.getDisplayValue('state') }); queue.push(childSysId); // enqueue children for processing } // gr goes out of scope here — eligible for GC immediately } return results; }, By using an explicit queue, only one GlideRecord object exists in memory at a time. Each iteration completes, the local gr falls out of scope, and the garbage collector can reclaim it before the next iteration begins. There is no call-stack growth regardless of hierarchy depth. Depth guard (if recursion cannot be avoided): _collectChildren: function(parentSysId, results, depth) { var MAX_DEPTH = 10; if (depth > MAX_DEPTH) { gs.warn('_collectChildren: max depth ' + MAX_DEPTH + ' reached at parent=' + parentSysId); return; } // ... rest of logic var gr = /* ... current GlideRecord ... */ this._collectChildren(gr.getUniqueValue(), results, depth + 1); }, Explicitly Clear Large Objects When Finished ServiceNow's server-side JavaScript engine is garbage-collected, but it cannot reclaim memory for objects that are still in scope. Long-running scripts — Scheduled Jobs, Fix Scripts, Background Scripts — can accumulate significant memory if large intermediate collections remain referenced throughout execution. Why This Matters: Variables declared with var at function/script scope persist for the lifetime of that scopeLarge arrays or objects built mid-script and never released delay GC even after the data is no longer neededIn long loops, unreleased objects compound memory pressure across iterations Best Practices: Null out large arrays and objects as soon as you are done with them (myArray = null; myObj = null;)Scope intermediate collections as narrowly as possible — keep processing logic in small, focused functions so variables go out of scope naturally when the function returns. Avoid re-using the same large array across multiple logical phases; release and re-create itAfter JSON serialization or bulk string building, null the source collection before continuing Example: // MEMORY ISSUE - Large array kept in scope long after use var allData = []; var gr = new GlideRecord('incident'); gr.addQuery('active', true); gr.query(); while (gr.next()) { allData.push({ sys_id: gr.getValue('sys_id'), number: gr.getValue('number') }); } var payload = JSON.stringify(allData); // allData still referenced sendToEndpoint(payload); // allData still referenced doMoreWork(); // allData still in memory here — wasteful // CORRECTION - NULL out the object when done var allData = []; var gr = new GlideRecord('incident'); gr.addQuery('active', true); gr.query(); while (gr.next()) { allData.push({ sys_id: gr.getValue('sys_id'), number: gr.getValue('number') }); } var payload = JSON.stringify(allData); allData = null; // release the source array before further work sendToEndpoint(payload); doMoreWork(); // allData no longer consuming heap Control Cache Sizes and Implement Expiration Script Includes and server-side utilities sometimes use object-level or static-like caches to avoid redundant queries. Without bounds, these caches grow unboundedly for the lifetime of the transaction or node session, consuming memory that is never recovered. Why This Matters: Unbounded caches accumulate entries proportional to query diversity, not a fixed sizeStale cache entries persist and consume memory long after the data they hold is relevantCached GlideElement or GlideRecord references carry the same overhead described in earlier sections Best Practices: Cap caches at a fixed maximum entry count and evict the oldest entries when the limit is reachedNever cache GlideRecord objects or GlideElement references — cache extracted primitive values onlyPrefer short-lived, transaction-scoped caches over node-level globals where possibleFor small, stable lookup tables (e.g. priority labels, state mappings), consider a bounded Script Include cache or system properties rather than a per-transaction GlideRecord query Example: // MEMORY ISSUE - Unbounded cache; stores GlideRecord-derived objects indefinitely var UserCache = (function() { var cache = {}; return { get: function(userId) { if (!cache[userId]) { var gr = new GlideRecord('sys_user'); if (gr.get(userId)) { cache[userId] = gr; // stores the GlideRecord object } } return cache[userId]; } }; })(); // CORRECT - Bounded cache storing only primitive values var UserCache = (function() { var cache = {}; var keys = []; var MAX_CACHE_SIZE = 200; return { get: function(userId) { if (!cache[userId]) { var gr = new GlideRecord('sys_user'); if (gr.get(userId)) { if (keys.length >= MAX_CACHE_SIZE) { delete cache[keys.shift()]; // evict oldest } cache[userId] = { name: gr.getDisplayValue('name'), email: gr.getValue('email') }; // primitives only keys.push(userId); } } return cache[userId]; } }; })(); Conclusion Memory management in ServiceNow is not optional—it's essential for platform stability and performance. The coding practices outlined in this article will help you avoid the most common memory-related issues that plague ServiceNow implementations. Key Takeaways: Always check for NULL values when passing variables to addQuery()Always extract primitive values from GlideRecord objects using getValue()Stringify dot-walked fields immediately to break object referencesUse setLimit() on every GlideRecord query that doesn't require all recordsImplement batching for large dataset operationsControl cache sizes and implement expiration policiesLimit recursion depth and prefer iterative approachesExplicitly clear large objects and arrays when finishedUse array join() instead of string concatenation in loops Remember that memory issues often don't appear during development with small datasets but manifest in production with real-world data volumes. Always test your code with production-scale datasets in sub-production environments, monitor transaction times, and use ServiceNow's transaction logs to identify memory-intensive operations. By following these practices consistently, you'll build ServiceNow applications that scale efficiently, perform reliably, and maintain system stability even under heavy load.