skills/development/performance-optimization/SKILL.md
Comprehensive guide to identifying and resolving ServiceNow performance bottlenecks including GlideRecord optimization, query tuning, index management, caching strategies, and profiling techniques
npx skillsauth add happy-technologies-llc/happy-servicenow-skills performance-optimizationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill provides a comprehensive approach to identifying and resolving performance issues in ServiceNow:
When to use: When experiencing slow form loads, list view delays, business rule timeouts, or general system sluggishness.
Who should use this: Developers, administrators, and performance engineers responsible for ServiceNow optimization.
Expected outcomes: Faster form loads, reduced transaction times, improved user experience, and lower system resource consumption.
admin (for index creation), itil or developer (for script analysis)| Metric | Target | Warning | Critical | |--------|--------|---------|----------| | Form Load Time | < 2 sec | 2-5 sec | > 5 sec | | List View Load | < 3 sec | 3-7 sec | > 7 sec | | Business Rule Execution | < 100 ms | 100-500 ms | > 500 ms | | Client Script Execution | < 200 ms | 200-500 ms | > 500 ms | | Query Response Time | < 50 ms | 50-200 ms | > 200 ms | | API Response Time | < 1 sec | 1-3 sec | > 3 sec |
Identify the slowest transactions in your instance:
Using MCP:
Tool: SN-Query-Table
Parameters:
table_name: sys_slow_transaction
query: sys_created_on>javascript:gs.daysAgo(7)
fields: url,response_time,user,client_transaction,sys_created_on
limit: 50
orderBy: response_time
orderByDesc: true
Analyze Results:
Find database queries consuming excessive time:
Using MCP:
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Query slow database operations
var slowQueries = [];
var gr = new GlideRecord('syslog_transaction');
gr.addQuery('sys_created_on', '>', gs.daysAgo(1));
gr.addQuery('type', 'sql');
gr.orderByDesc('response_time');
gr.setLimit(50);
gr.query();
while (gr.next()) {
slowQueries.push({
table: gr.url.toString().split(' ')[0],
response_time: gr.response_time.toString(),
query: gr.url.toString().substring(0, 200),
created: gr.sys_created_on.toString()
});
}
gs.info('Slow Queries Report:\n' + JSON.stringify(slowQueries, null, 2));
description: Identify slow database queries
For detailed transaction analysis:
Manual Steps (UI Required):
Query Debug Output:
Tool: SN-Query-Table
Parameters:
table_name: syslog
query: sourceLIKEDebug^sys_created_on>javascript:gs.minutesAgo(15)
fields: message,source,sys_created_on
limit: 100
Check for quota violations:
Using MCP:
Tool: SN-Query-Table
Parameters:
table_name: syslog
query: messageLIKEquota^sys_created_on>javascript:gs.daysAgo(1)
fields: message,source,sys_created_on
limit: 50
Common Quota Violations: | Quota | Default Limit | Symptom | |-------|---------------|---------| | SQL statements | 1,000 per transaction | N+1 query pattern | | Script execution | 180 seconds | Long-running scripts | | Memory | 100 MB | Large data processing | | Outbound HTTP | 10 per transaction | Integration loops |
Always limit results to prevent full table scans:
// BAD: No limit - scans entire table
var gr = new GlideRecord('incident');
gr.addQuery('active', true);
gr.query();
// GOOD: Explicit limit
var gr = new GlideRecord('incident');
gr.addQuery('active', true);
gr.setLimit(100); // Only get what you need
gr.query();
When to use different limits: | Use Case | Recommended Limit | |----------|-------------------| | Display in UI list | 20-50 | | Batch processing | 100-500 | | Aggregation input | 1000 max | | Existence check | 1 |
Only retrieve fields you need:
// BAD: Gets all 100+ fields
var gr = new GlideRecord('incident');
gr.addQuery('priority', 1);
gr.query();
// GOOD: Gets only needed fields (70%+ faster)
var gr = new GlideRecord('incident');
gr.addQuery('priority', 1);
gr.setFields('sys_id,number,short_description,assigned_to');
gr.query();
Using MCP:
Tool: SN-Query-Table
Parameters:
table_name: incident
query: active=true^priority=1
fields: sys_id,number,short_description,assigned_to
limit: 50
For simple existence checks, avoid iteration:
// BAD: Loads record just to check existence
var gr = new GlideRecord('incident');
gr.addQuery('number', 'INC0012345');
gr.query();
if (gr.next()) {
// Exists
}
// GOOD: Use getRowCount() for existence
var gr = new GlideRecord('incident');
gr.addQuery('number', 'INC0012345');
gr.setLimit(1);
gr.query();
if (gr.getRowCount() > 0) {
// Exists
}
// BEST: Use get() for single record by unique field
var gr = new GlideRecord('incident');
if (gr.get('number', 'INC0012345')) {
// Exists and loaded
}
Never iterate to count records:
// BAD: Loads all records just to count (can be 1000x slower)
var count = 0;
var gr = new GlideRecord('incident');
gr.addQuery('active', true);
gr.query();
while (gr.next()) {
count++;
}
// GOOD: Use GlideAggregate
var ga = new GlideAggregate('incident');
ga.addQuery('active', true);
ga.addAggregate('COUNT');
ga.query();
var count = 0;
if (ga.next()) {
count = parseInt(ga.getAggregate('COUNT'));
}
Advanced Aggregation Example:
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Get incident counts grouped by priority
var stats = {};
var ga = new GlideAggregate('incident');
ga.addQuery('active', true);
ga.addAggregate('COUNT');
ga.groupBy('priority');
ga.query();
while (ga.next()) {
var priority = ga.priority.getDisplayValue() || 'Unset';
var count = parseInt(ga.getAggregate('COUNT'));
stats[priority] = count;
}
gs.info('Incident Stats by Priority:\n' + JSON.stringify(stats, null, 2));
description: Count incidents by priority using GlideAggregate
The N+1 anti-pattern:
// BAD: N+1 query pattern (1 + N database calls)
var incidents = new GlideRecord('incident');
incidents.addQuery('active', true);
incidents.setLimit(100);
incidents.query();
while (incidents.next()) {
// This causes a separate query for EACH incident
var user = new GlideRecord('sys_user');
user.get(incidents.assigned_to); // +1 query per iteration
gs.info(incidents.number + ' - ' + user.name);
}
// Total queries: 1 + 100 = 101 queries!
Use dot-walking for reference fields:
// GOOD: Single query with dot-walking
var incidents = new GlideRecord('incident');
incidents.addQuery('active', true);
incidents.setLimit(100);
incidents.query();
while (incidents.next()) {
// Dot-walking retrieves reference data without extra queries
var userName = incidents.assigned_to.name;
var userEmail = incidents.assigned_to.email;
gs.info(incidents.number + ' - ' + userName);
}
// Total queries: 1 query!
For complex scenarios, load all references first:
// GOOD: Batch loading pattern
// Step 1: Collect all unique sys_ids
var userIds = [];
var incidents = new GlideRecord('incident');
incidents.addQuery('active', true);
incidents.addQuery('assigned_to', '!=', '');
incidents.setLimit(100);
incidents.query();
while (incidents.next()) {
var userId = incidents.assigned_to.toString();
if (userIds.indexOf(userId) === -1) {
userIds.push(userId);
}
}
// Step 2: Load all users in single query
var userMap = {};
if (userIds.length > 0) {
var users = new GlideRecord('sys_user');
users.addQuery('sys_id', 'IN', userIds.join(','));
users.query();
while (users.next()) {
userMap[users.sys_id.toString()] = {
name: users.name.toString(),
email: users.email.toString()
};
}
}
// Total queries: 2 queries regardless of record count!
Detect N+1 patterns in your code:
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Analyze business rules for potential N+1 patterns
var suspects = [];
var gr = new GlideRecord('sys_script');
gr.addQuery('active', true);
gr.addEncodedQuery('scriptLIKEwhile*next*^scriptLIKEnew GlideRecord');
gr.setLimit(50);
gr.query();
while (gr.next()) {
// Check for GlideRecord inside while loops
var script = gr.script.toString();
var lines = script.split('\n');
var inWhileLoop = false;
var hasInnerGR = false;
for (var i = 0; i < lines.length; i++) {
if (lines[i].indexOf('while') > -1 && lines[i].indexOf('.next()') > -1) {
inWhileLoop = true;
}
if (inWhileLoop && lines[i].indexOf('new GlideRecord') > -1) {
hasInnerGR = true;
break;
}
if (inWhileLoop && lines[i].indexOf('}') > -1) {
inWhileLoop = false;
}
}
if (hasInnerGR) {
suspects.push({
name: gr.name.toString(),
table: gr.collection.toString(),
sys_id: gr.sys_id.toString()
});
}
}
gs.info('Potential N+1 Patterns Found: ' + suspects.length);
gs.info(JSON.stringify(suspects, null, 2));
description: Detect potential N+1 query patterns in business rules
Find frequently queried fields without indexes:
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Analyze slow queries for missing indexes
var fieldUsage = {};
var gr = new GlideRecord('syslog_transaction');
gr.addQuery('type', 'sql');
gr.addQuery('sys_created_on', '>', gs.daysAgo(1));
gr.addQuery('response_time', '>', 100); // > 100ms
gr.setLimit(500);
gr.query();
while (gr.next()) {
var url = gr.url.toString();
// Extract WHERE clause fields
var whereMatch = url.match(/WHERE\s+(.+?)(?:ORDER|LIMIT|$)/i);
if (whereMatch) {
var conditions = whereMatch[1].split(/\s+AND\s+/i);
conditions.forEach(function(cond) {
var fieldMatch = cond.match(/^(\w+)/);
if (fieldMatch) {
var field = fieldMatch[1];
fieldUsage[field] = (fieldUsage[field] || 0) + 1;
}
});
}
}
// Sort by usage
var sorted = Object.keys(fieldUsage).sort(function(a,b) {
return fieldUsage[b] - fieldUsage[a];
});
gs.info('Most queried fields (potential index candidates):');
sorted.slice(0, 20).forEach(function(field) {
gs.info(' ' + field + ': ' + fieldUsage[field] + ' queries');
});
description: Identify index candidates from slow queries
Check current indexes on a table:
Tool: SN-Query-Table
Parameters:
table_name: sys_db_index
query: table=incident
fields: name,column_name,unique_index,active
limit: 50
Fields that should be indexed: | Field Type | Index Priority | Example | |------------|----------------|---------| | Foreign keys | High | assigned_to, caller_id | | Status fields | High | state, active | | Date filters | Medium | opened_at, resolved_at | | Category fields | Medium | category, subcategory | | Number fields | Low | number (usually auto-indexed) |
Index Creation Guidelines:
Create Index (Admin Required):
Tool: SN-Create-Record
Parameters:
table_name: sys_db_index
data:
table: incident
column_name: assignment_group
active: true
Find slow synchronous business rules:
Tool: SN-Query-Table
Parameters:
table_name: sys_script
query: active=true^when!=async^collection=incident
fields: name,when,order,sys_id
limit: 50
Criteria for async conversion: | Can Be Async | Must Stay Sync | |--------------|----------------| | Notifications | Field calculations | | Logging/auditing | Data validation | | External integrations | Workflow triggers | | Non-critical updates | Security checks | | Report generation | Display business rules |
Async Business Rule Pattern:
// Async business rule example
// When: async
// Order: 100
(function executeRule(current, previous /*null when async*/) {
// Safe for async - doesn't affect transaction
var event = new GlideRecord('sysevent');
event.initialize();
event.name = 'incident.updated';
event.parm1 = current.sys_id.toString();
event.parm2 = current.number.toString();
event.insert();
// External notification
var restMessage = new sn_ws.RESTMessageV2('External System', 'notify');
restMessage.setStringParameter('incident_id', current.sys_id.toString());
restMessage.executeAsync(); // Non-blocking
})(current, previous);
// OPTIMIZED Business Rule Template
(function executeRule(current, previous) {
// 1. Exit early if field hasn't changed
if (previous && !current.state.changes()) {
return; // No work needed
}
// 2. Exit early for bulk operations
if (current.operation() == 'update' && !gs.isInteractive()) {
// Consider if this should run for imports/scripts
}
// 3. Use efficient existence checks
if (!current.assigned_to.nil()) {
// Only process if assigned
}
// 4. Cache repeated calculations
var state = current.state.toString(); // Store once
// 5. Avoid GlideRecord in loops
// Use dot-walking or batch loading instead
// 6. Set fields directly when possible
current.work_notes = 'State changed to: ' + current.state.getDisplayValue();
// Don't update() - let the transaction handle it
})(current, previous);
Anti-pattern:
// BAD: O(n*m) complexity
var incidents = new GlideRecord('incident');
incidents.query();
while (incidents.next()) {
var tasks = new GlideRecord('sc_task');
tasks.query();
while (tasks.next()) {
// Extremely slow: checks every task for every incident
if (tasks.request_item == incidents.sys_id) {
// process
}
}
}
Optimized Pattern:
// GOOD: O(n+m) complexity
// Build lookup map first
var tasksByRequest = {};
var tasks = new GlideRecord('sc_task');
tasks.query();
while (tasks.next()) {
var reqId = tasks.request_item.toString();
if (!tasksByRequest[reqId]) {
tasksByRequest[reqId] = [];
}
tasksByRequest[reqId].push(tasks.sys_id.toString());
}
// Now iterate incidents with O(1) lookups
var incidents = new GlideRecord('incident');
incidents.query();
while (incidents.next()) {
var relatedTasks = tasksByRequest[incidents.sys_id.toString()] || [];
// process related tasks
}
Avoid repeated string concatenation:
// BAD: Creates new string object each iteration
var result = '';
var gr = new GlideRecord('incident');
gr.setLimit(1000);
gr.query();
while (gr.next()) {
result += gr.number + ', '; // Memory inefficient
}
// GOOD: Use array join
var results = [];
var gr = new GlideRecord('incident');
gr.setLimit(1000);
gr.query();
while (gr.next()) {
results.push(gr.number.toString());
}
var result = results.join(', ');
Handle large JSON efficiently:
// BAD: Parse entire JSON into memory
var bigJson = gr.large_json_field.toString();
var data = JSON.parse(bigJson); // Entire object in memory
// GOOD: Stream processing for large data
// Use GlideStringUtil for validation
var jsonField = gr.large_json_field.toString();
if (GlideStringUtil.isJSON(jsonField)) {
// Process in chunks if possible
// Or use GlideElement methods
}
// BEST: Store structured data in related records
// instead of large JSON blobs
Efficient Client Scripts:
// GOOD: Client script with performance optimizations
function onChange(control, oldValue, newValue, isLoading) {
// 1. Skip during form load
if (isLoading) {
return;
}
// 2. Skip if value unchanged
if (oldValue == newValue) {
return;
}
// 3. Use g_form caching
var priority = g_form.getValue('priority'); // Cached locally
// 4. Batch field updates
g_form.setReadOnly('resolution_code', true);
g_form.setMandatory('resolution_notes', true);
// Multiple setX calls are batched automatically
// 5. Avoid synchronous GlideAjax
var ga = new GlideAjax('IncidentUtils');
ga.addParam('sysparm_name', 'getDefaultAssignee');
ga.addParam('sysparm_priority', priority);
ga.getXMLAnswer(function(answer) {
// Async callback - doesn't block UI
if (answer) {
g_form.setValue('assigned_to', answer);
}
});
}
Choose the right tool: | Use Case | Recommended | Reason | |----------|-------------|--------| | Show/Hide fields | UI Policy | Declarative, faster | | Set mandatory | UI Policy | No JavaScript overhead | | Simple conditions | UI Policy | Cached, no network | | Complex logic | Client Script | More flexibility | | GlideAjax calls | Client Script | Required for server data | | Field calculations | Client Script | Dynamic computation |
Form Performance Checklist:
Query Slow Forms:
Tool: SN-Query-Table
Parameters:
table_name: sys_slow_transaction
query: urlLIKEform^sys_created_on>javascript:gs.daysAgo(7)
fields: url,response_time,user
limit: 50
orderByDesc: response_time
Use gs.getProperty with caching:
// Properties are cached automatically
var maxRecords = gs.getProperty('custom.max_records', '100');
// For frequently accessed properties, consider Script Include caching
var CachedConfig = Class.create();
CachedConfig.prototype = {
initialize: function() {
this.cache = {};
},
get: function(name, defaultValue) {
if (!this.cache[name]) {
this.cache[name] = gs.getProperty(name, defaultValue);
}
return this.cache[name];
},
type: 'CachedConfig'
};
Leverage built-in caching:
// Cache expensive calculations
var cacheKey = 'user_permissions_' + gs.getUserID();
var cached = gs.getSession().getClientData(cacheKey);
if (!cached) {
// Calculate permissions (expensive)
var permissions = calculateUserPermissions();
gs.getSession().putClientData(cacheKey, JSON.stringify(permissions));
cached = JSON.stringify(permissions);
}
var userPermissions = JSON.parse(cached);
Optimize reference qualifiers:
// BAD: Expensive query runs every time
javascript:new MyUtil().getValidAssignees()
// GOOD: Use encoded query directly
active=true^roles=itil
// BEST: Cache complex qualifiers
javascript:gs.getProperty('incident.assignee_qualifier', 'active=true')
Monitor key metrics:
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Performance Health Check
var report = {
timestamp: new GlideDateTime().getDisplayValue(),
metrics: {}
};
// 1. Slow transactions today
var slowTx = new GlideAggregate('sys_slow_transaction');
slowTx.addQuery('sys_created_on', '>', gs.beginningOfToday());
slowTx.addAggregate('COUNT');
slowTx.addAggregate('AVG', 'response_time');
slowTx.query();
if (slowTx.next()) {
report.metrics.slow_transactions = {
count: parseInt(slowTx.getAggregate('COUNT')),
avg_response_time: Math.round(parseFloat(slowTx.getAggregate('AVG', 'response_time')))
};
}
// 2. Business rule execution times
var brLogs = new GlideAggregate('syslog_transaction');
brLogs.addQuery('type', 'business rule');
brLogs.addQuery('sys_created_on', '>', gs.beginningOfToday());
brLogs.addAggregate('COUNT');
brLogs.addAggregate('AVG', 'response_time');
brLogs.query();
if (brLogs.next()) {
report.metrics.business_rules = {
count: parseInt(brLogs.getAggregate('COUNT')),
avg_execution_time: Math.round(parseFloat(brLogs.getAggregate('AVG', 'response_time')))
};
}
// 3. Error rate
var errors = new GlideAggregate('syslog');
errors.addQuery('level', '2'); // Error level
errors.addQuery('sys_created_on', '>', gs.beginningOfToday());
errors.addAggregate('COUNT');
errors.query();
if (errors.next()) {
report.metrics.errors_today = parseInt(errors.getAggregate('COUNT'));
}
// 4. Active scheduled jobs
var jobs = new GlideAggregate('sys_trigger');
jobs.addQuery('state', '0'); // Ready
jobs.addAggregate('COUNT');
jobs.query();
if (jobs.next()) {
report.metrics.pending_jobs = parseInt(jobs.getAggregate('COUNT'));
}
gs.info('Performance Report:\n' + JSON.stringify(report, null, 2));
description: Generate performance health report
Create quota violation alerts:
Tool: SN-Create-Record
Parameters:
table_name: sysevent_script_action
data:
name: Performance Quota Alert
event_name: performance.quota.exceeded
script: |
// Send alert on quota violations
var quota_type = event.parm1;
var details = event.parm2;
gs.eventQueue('custom.performance.alert', null, quota_type, details);
| Operation | MCP Tool | Purpose | |-----------|----------|---------| | Query Slow Transactions | SN-Query-Table | Find performance bottlenecks | | Analyze Queries | SN-Execute-Background-Script | Deep analysis scripts | | Check Schema | SN-Get-Table-Schema | Verify field types | | Discover Indexes | SN-Discover-Table-Schema | Find existing indexes | | Create Index | SN-Create-Record | Add missing indexes |
Symptom: Form takes > 5 seconds to load Causes:
Solution:
Tool: SN-Query-Table
Parameters:
table_name: sys_ui_related_list
query: table=incident
fields: related_list,order,view
limit: 50
Review and reduce related lists, set lower default counts.
Symptom: Transaction times out after 180 seconds Causes:
Solution:
Symptom: "Memory quota exceeded" errors Causes:
Solution:
// Process in batches
var BATCH_SIZE = 200;
var offset = 0;
var hasMore = true;
while (hasMore) {
var gr = new GlideRecord('incident');
gr.addQuery('active', true);
gr.orderBy('sys_created_on');
gr.chooseWindow(offset, offset + BATCH_SIZE);
gr.query();
var count = 0;
while (gr.next()) {
// Process record
count++;
}
hasMore = (count == BATCH_SIZE);
offset += BATCH_SIZE;
// Force garbage collection hint
gs.nil();
}
Symptom: List views take > 7 seconds to load Causes:
Solution:
| Practice | Impact | Difficulty | |----------|--------|------------| | Add setLimit() | High | Easy | | Use setFields() | High | Easy | | Fix N+1 queries | Critical | Medium | | Add indexes | High | Medium | | Convert to async | Medium | Medium | | Optimize loops | High | Medium | | Enable caching | Medium | Medium | | Client script async | Medium | Easy |
admin/script-execution - Background script execution patternsadmin/batch-operations - Efficient bulk record operationsadmin/schema-discovery - Understanding table structuresdevelopment/debugging-techniques - Troubleshooting scriptstesting
Manage supplier onboarding, qualification, performance monitoring, and offboarding with auditable lifecycle controls
tools
Identify emerging risks, prioritize intake signals, and route candidates into formal GRC risk assessment workflows
documentation
Screen inbound documents for completeness, policy risk, and routing readiness before extraction or case workflows
testing
Generate concise task summaries with status, timeline, blockers, SLA risk, and recommended next actions