Skip to Content
  1. Home
  2. /
  3. Blog
  4. /
  5. Session Caching in ServiceNow: Boost Performance with Script Includes and Before Query Business Rules
Sunday, October 19, 2025

Session Caching in ServiceNow: Boost Performance with Script Includes and Before Query Business Rules

Performance optimization is a critical concern for any ServiceNow implementation. As your instance grows and user activity increases, you may notice slowdowns in list views, forms, and other operations. One of the most effective yet underutilized techniques for improving performance is session caching. In this comprehensive guide, I'll show you how to leverage session caching with Script Includes and Before Query Business Rules to achieve dramatic performance improvements—in some cases, up to 65% faster transaction processing.

Understanding Session Caching in ServiceNow

Session caching in ServiceNow allows you to store data in a user's session memory, making it available throughout their session without repeatedly querying the database. This is particularly powerful when you need to access the same data multiple times during a single user session.

The Session Cache API

ServiceNow provides two primary methods for session caching:

  • putClientData(key, value): Stores data in the user's session
  • getClientData(key): Retrieves data from the user's session

These methods are available through the GlideSession API, accessible via gs.getSession() on the server-side, or g_user on the client-side.

JavaScript
// Server-side example
var session = gs.getSession();
session.putClientData('userRegion', 'EMEA');
var region = session.getClientData('userRegion');

// Client-side example
var region = g_user.getClientData('userRegion');

Why Session Caching Matters

According to ServiceNow performance studies, implementing proper caching strategies can improve transaction processing time by 55-65%. This dramatic improvement comes from:

  1. Reduced database queries: Each query has overhead in connection time, parsing, execution, and network latency
  2. Faster memory access: Reading from memory is orders of magnitude faster than database queries
  3. Lower server load: Fewer database queries mean reduced load on the database server
  4. Better user experience: Faster response times lead to happier users

The Problem: Before Query Business Rules Without Caching

Before Query Business Rules are powerful tools for filtering data based on custom logic. However, they come with significant performance challenges:

Why Before Query Business Rules Are Expensive

Unlike Access Control Lists (ACLs) which have built-in application tier caching, Before Query Business Rules execute every single time a table is queried. Consider this scenario:

JavaScript
// Before Query Business Rule on incident table
(function executeRule(current, previous /*null when async*/) {
    // Get user's region from the database
    var userGR = new GlideRecord('sys_user');
    userGR.get(gs.getUserID());
    var userRegion = userGR.getValue('u_region');

    // Filter incidents to only show records from user's region
    current.addQuery('u_region', userRegion);
})(current, previous);

This seems reasonable, but consider what happens when:

  • A user opens a list view showing 50 incidents
  • Each incident loads related lists (tasks, approvals, etc.)
  • The user refreshes the page
  • Multiple users are accessing the system simultaneously

That simple userGR.get() query could execute hundreds or thousands of times per hour for a single user, and each execution requires a database query. This is where session caching becomes essential.

Solution: Session Caching with Script Includes

The most effective pattern for implementing session caching is to create a reusable Script Include that handles the caching logic. This centralizes your caching strategy and makes it easy to use across multiple Business Rules.

Creating a Session Cache Script Include

Here's a robust Script Include for managing session-cached data:

JavaScript
var SessionCacheUtil = Class.create();
SessionCacheUtil.prototype = {
    initialize: function() {
        this.session = gs.getSession();
        this.CACHE_PREFIX = 'cache_';
    },

    /**
     * Get data from session cache or execute callback to fetch and cache it
     * @param {string} key - Cache key
     * @param {function} fetchCallback - Function to execute if cache miss
     * @param {number} ttl - Time to live in seconds (optional)
     * @returns {*} Cached or freshly fetched data
     */
    getOrSet: function(key, fetchCallback, ttl) {
        var cacheKey = this.CACHE_PREFIX + key;
        var cachedValue = this.session.getClientData(cacheKey);

        // Check if we have cached data
        if (cachedValue !== null && cachedValue !== undefined && cachedValue !== '') {
            // If TTL is specified, check if cache is still valid
            if (ttl) {
                var cacheTimeKey = cacheKey + '_timestamp';
                var cacheTime = this.session.getClientData(cacheTimeKey);

                if (cacheTime) {
                    var currentTime = new GlideDateTime().getNumericValue();
                    var cachedTime = parseInt(cacheTime);
                    var age = (currentTime - cachedTime) / 1000; // Convert to seconds

                    if (age > ttl) {
                        gs.debug('SessionCacheUtil: Cache expired for key: ' + key);
                        // Cache expired, fetch new data
                        return this._fetchAndCache(key, fetchCallback, ttl);
                    }
                }
            }

            gs.debug('SessionCacheUtil: Cache hit for key: ' + key);
            return cachedValue;
        }

        gs.debug('SessionCacheUtil: Cache miss for key: ' + key);
        return this._fetchAndCache(key, fetchCallback, ttl);
    },

    /**
     * Fetch data using callback and store in cache
     * @param {string} key - Cache key
     * @param {function} fetchCallback - Function to execute
     * @param {number} ttl - Time to live in seconds (optional)
     * @returns {*} Fetched data
     */
    _fetchAndCache: function(key, fetchCallback, ttl) {
        var value = fetchCallback();

        // Only cache strings and numbers (immutable types)
        if (typeof value === 'string' || typeof value === 'number') {
            var cacheKey = this.CACHE_PREFIX + key;
            this.session.putClientData(cacheKey, value);

            // Store timestamp if TTL is specified
            if (ttl) {
                var cacheTimeKey = cacheKey + '_timestamp';
                var currentTime = new GlideDateTime().getNumericValue();
                this.session.putClientData(cacheTimeKey, currentTime.toString());
            }

            gs.debug('SessionCacheUtil: Cached value for key: ' + key);
        } else {
            gs.warn('SessionCacheUtil: Cannot cache non-primitive type for key: ' + key);
        }

        return value;
    },

    /**
     * Get data from cache
     * @param {string} key - Cache key
     * @returns {*} Cached data or null
     */
    get: function(key) {
        var cacheKey = this.CACHE_PREFIX + key;
        return this.session.getClientData(cacheKey);
    },

    /**
     * Set data in cache
     * @param {string} key - Cache key
     * @param {*} value - Value to cache (must be string or number)
     */
    set: function(key, value) {
        if (typeof value === 'string' || typeof value === 'number') {
            var cacheKey = this.CACHE_PREFIX + key;
            this.session.putClientData(cacheKey, value);
        } else {
            gs.warn('SessionCacheUtil: Cannot cache non-primitive type for key: ' + key);
        }
    },

    /**
     * Remove data from cache
     * @param {string} key - Cache key
     */
    remove: function(key) {
        var cacheKey = this.CACHE_PREFIX + key;
        this.session.putClientData(cacheKey, null);

        // Also remove timestamp if it exists
        var cacheTimeKey = cacheKey + '_timestamp';
        this.session.putClientData(cacheTimeKey, null);
    },

    /**
     * Clear all cached data (use with caution)
     */
    clear: function() {
        this.session.clearClientData();
    },

    type: 'SessionCacheUtil'
};

Using the Session Cache Script Include in Before Query Business Rules

Now let's transform our earlier inefficient Before Query Business Rule:

JavaScript
// Before Query Business Rule on incident table
// Name: Filter Incidents by User Region
// When: before
// Query: true
// Order: 100

(function executeRule(current, previous /*null when async*/) {
    var cache = new SessionCacheUtil();

    // Get user region from cache or database
    var userRegion = cache.getOrSet('user_region', function() {
        var userGR = new GlideRecord('sys_user');
        if (userGR.get(gs.getUserID())) {
            return userGR.getValue('u_region');
        }
        return '';
    });

    // Filter incidents to only show records from user's region
    if (userRegion) {
        current.addQuery('u_region', userRegion);
    }
})(current, previous);

The difference? The database query for the user's region now executes once per session instead of hundreds or thousands of times. After the first query, all subsequent executions read from the in-memory session cache.

Real-World Examples and Patterns

Example 1: Caching Company Hierarchy

A common use case is filtering records based on a company hierarchy. Users should only see records from their company and all child companies.

JavaScript
// Script Include: CompanyHierarchyUtil
var CompanyHierarchyUtil = Class.create();
CompanyHierarchyUtil.prototype = {
    initialize: function() {
        this.cache = new SessionCacheUtil();
    },

    /**
     * Get all company sys_ids in user's hierarchy (comma-separated)
     * @returns {string} Comma-separated list of company sys_ids
     */
    getUserCompanyHierarchy: function() {
        return this.cache.getOrSet('user_company_hierarchy', function() {
            var companies = [];
            var userCompanyId = gs.getUser().getCompanyID();

            if (!userCompanyId) {
                return '';
            }

            // Add user's company
            companies.push(userCompanyId);

            // Get all child companies recursively
            this._getChildCompanies(userCompanyId, companies);

            return companies.join(',');
        }.bind(this));
    },

    /**
     * Recursively get all child companies
     * @param {string} parentId - Parent company sys_id
     * @param {Array} companies - Array to store company sys_ids
     */
    _getChildCompanies: function(parentId, companies) {
        var companyGR = new GlideRecord('core_company');
        companyGR.addQuery('parent', parentId);
        companyGR.query();

        while (companyGR.next()) {
            var childId = companyGR.getValue('sys_id');
            companies.push(childId);

            // Recursively get children of this company
            this._getChildCompanies(childId, companies);
        }
    },

    type: 'CompanyHierarchyUtil'
};

Now use it in a Before Query Business Rule:

JavaScript
// Before Query Business Rule on contract table
(function executeRule(current, previous /*null when async*/) {
    var hierarchyUtil = new CompanyHierarchyUtil();
    var companyHierarchy = hierarchyUtil.getUserCompanyHierarchy();

    if (companyHierarchy) {
        current.addQuery('company', 'IN', companyHierarchy);
    }
})(current, previous);

Example 2: Caching User Department and Location

Filter records based on multiple user attributes:

JavaScript
// Script Include: UserContextCache
var UserContextCache = Class.create();
UserContextCache.prototype = {
    initialize: function() {
        this.cache = new SessionCacheUtil();
    },

    /**
     * Get user's department sys_id
     * @returns {string} Department sys_id
     */
    getUserDepartment: function() {
        return this.cache.getOrSet('user_department', function() {
            var user = gs.getUser();
            return user.getDepartmentID() || '';
        });
    },

    /**
     * Get user's location sys_id
     * @returns {string} Location sys_id
     */
    getUserLocation: function() {
        return this.cache.getOrSet('user_location', function() {
            var userGR = new GlideRecord('sys_user');
            if (userGR.get(gs.getUserID())) {
                return userGR.getValue('location') || '';
            }
            return '';
        });
    },

    /**
     * Get user's manager sys_id
     * @returns {string} Manager sys_id
     */
    getUserManager: function() {
        return this.cache.getOrSet('user_manager', function() {
            var userGR = new GlideRecord('sys_user');
            if (userGR.get(gs.getUserID())) {
                return userGR.getValue('manager') || '';
            }
            return '';
        });
    },

    /**
     * Check if user is in a specific group (cached)
     * @param {string} groupName - Group name
     * @returns {boolean} True if user is in group
     */
    isUserInGroup: function(groupName) {
        var cacheKey = 'user_in_group_' + groupName;
        var result = this.cache.get(cacheKey);

        if (result !== null && result !== undefined) {
            return result === 'true';
        }

        var inGroup = gs.getUser().isMemberOf(groupName);
        this.cache.set(cacheKey, inGroup.toString());

        return inGroup;
    },

    type: 'UserContextCache'
};

Use in Before Query Business Rule:

JavaScript
// Before Query Business Rule on kb_knowledge table
(function executeRule(current, previous /*null when async*/) {
    var userContext = new UserContextCache();

    // Only show knowledge articles from user's department or location
    var dept = userContext.getUserDepartment();
    var location = userContext.getUserLocation();

    if (dept || location) {
        var condition = current.addQuery('u_department', dept);
        condition.addOrCondition('u_location', location);
    }
})(current, previous);

Example 3: Caching Complex Calculations

Some Before Query Business Rules perform complex calculations that don't change during a session:

JavaScript
// Script Include: TerritoryCalculator
var TerritoryCalculator = Class.create();
TerritoryCalculator.prototype = {
    initialize: function() {
        this.cache = new SessionCacheUtil();
    },

    /**
     * Calculate user's sales territories based on complex logic
     * @returns {string} Comma-separated list of territory sys_ids
     */
    getUserTerritories: function() {
        return this.cache.getOrSet('user_territories', function() {
            var territories = [];
            var userId = gs.getUserID();

            // Complex query to determine territories
            // This might involve multiple tables, joins, or calculations
            var territoryGR = new GlideRecord('u_territory_assignment');
            territoryGR.addQuery('assigned_to', userId);
            territoryGR.addActiveQuery();
            territoryGR.query();

            while (territoryGR.next()) {
                territories.push(territoryGR.getValue('territory'));
            }

            // Also check if user's manager has territories they inherit
            var userGR = new GlideRecord('sys_user');
            if (userGR.get(userId)) {
                var managerId = userGR.getValue('manager');
                if (managerId) {
                    var managerTerritoryGR = new GlideRecord('u_territory_assignment');
                    managerTerritoryGR.addQuery('assigned_to', managerId);
                    managerTerritoryGR.addQuery('inheritable', true);
                    managerTerritoryGR.addActiveQuery();
                    managerTerritoryGR.query();

                    while (managerTerritoryGR.next()) {
                        var terrId = managerTerritoryGR.getValue('territory');
                        if (territories.indexOf(terrId) === -1) {
                            territories.push(terrId);
                        }
                    }
                }
            }

            return territories.join(',');
        });
    },

    type: 'TerritoryCalculator'
};

Best Practices and Important Considerations

1. Only Cache Immutable Data Types

For caching purposes, you should only use immutable data types—in JavaScript, these are strings and numbers. All other data types are internally represented by references, which can lead to:

  • The cached data being changed outside the cache
  • Memory leaks
  • Unexpected behavior
JavaScript
// Good: Cache strings and numbers
cache.set('user_region', 'EMEA');
cache.set('user_score', 100);

// Bad: Don't cache objects or arrays
cache.set('user_object', {name: 'John'}); // DON'T DO THIS
cache.set('company_list', ['comp1', 'comp2']); // DON'T DO THIS

// Good alternative: Convert to string
cache.set('company_list', 'comp1,comp2');

2. Be Mindful of Memory Limitations

User sessions run on application nodes with limited memory (2GB heap size). Total accumulated memory per node should be less than 1MB—preferably much less.

Guidelines:

  • Only cache small pieces of data
  • Cache data that's accessed frequently
  • Don't cache large text blocks or complex data structures
  • Monitor your instance's memory usage

3. Cache Data That Doesn't Change Frequently

Session caching is ideal for data that is:

  • Static during a user's session (user's department, region, company)
  • Expensive to calculate
  • Accessed multiple times

Don't cache:

  • Data that changes frequently during a session
  • Data that's only needed once
  • Real-time data that must always be current

4. Implement Cache Invalidation When Needed

If cached data can change during a session, implement a strategy to clear the cache:

JavaScript
// Business Rule on sys_user table (after update)
// Clear cached user data when user record is updated
(function executeRule(current, previous) {
    // Only clear cache if relevant fields changed
    if (current.u_region.changes() ||
        current.department.changes() ||
        current.location.changes()) {

        // Clear cache for this specific user
        // This requires the user to be the current session user
        if (current.sys_id == gs.getUserID()) {
            var cache = new SessionCacheUtil();
            cache.remove('user_region');
            cache.remove('user_department');
            cache.remove('user_location');
        }
    }
})(current, previous);

5. Use Descriptive Cache Keys

Use clear, descriptive cache keys that indicate what's being cached:

JavaScript
// Good
cache.getOrSet('user_region', fetchFunction);
cache.getOrSet('user_company_hierarchy', fetchFunction);
cache.getOrSet('user_in_group_itil', fetchFunction);

// Bad
cache.getOrSet('data1', fetchFunction);
cache.getOrSet('temp', fetchFunction);
cache.getOrSet('x', fetchFunction);

6. Add Debug Logging

Include debug logging in your caching logic to help troubleshoot issues:

JavaScript
getOrSet: function(key, fetchCallback, ttl) {
    var cacheKey = this.CACHE_PREFIX + key;
    var cachedValue = this.session.getClientData(cacheKey);

    if (cachedValue !== null && cachedValue !== undefined && cachedValue !== '') {
        gs.debug('SessionCache: HIT for key=' + key + ', value=' + cachedValue);
        return cachedValue;
    }

    gs.debug('SessionCache: MISS for key=' + key + ', fetching data');
    return this._fetchAndCache(key, fetchCallback, ttl);
}

7. Consider Using TTL for Semi-Static Data

For data that changes occasionally, implement a Time-To-Live (TTL) mechanism:

JavaScript
// Cache user's region for 5 minutes (300 seconds)
var userRegion = cache.getOrSet('user_region', function() {
    var userGR = new GlideRecord('sys_user');
    if (userGR.get(gs.getUserID())) {
        return userGR.getValue('u_region');
    }
    return '';
}, 300); // TTL of 300 seconds

Measuring Performance Improvements

To measure the impact of session caching, use the following approach:

Before Implementing Caching

JavaScript
// Add timing to your Before Query Business Rule
(function executeRule(current, previous) {
    var startTime = new GlideDateTime().getNumericValue();

    // Your existing logic without caching
    var userGR = new GlideRecord('sys_user');
    userGR.get(gs.getUserID());
    var userRegion = userGR.getValue('u_region');
    current.addQuery('u_region', userRegion);

    var endTime = new GlideDateTime().getNumericValue();
    var duration = endTime - startTime;
    gs.info('Before Query BR execution time (no cache): ' + duration + 'ms');
})(current, previous);

After Implementing Caching

JavaScript
(function executeRule(current, previous) {
    var startTime = new GlideDateTime().getNumericValue();

    var cache = new SessionCacheUtil();
    var userRegion = cache.getOrSet('user_region', function() {
        var userGR = new GlideRecord('sys_user');
        if (userGR.get(gs.getUserID())) {
            return userGR.getValue('u_region');
        }
        return '';
    });
    current.addQuery('u_region', userRegion);

    var endTime = new GlideDateTime().getNumericValue();
    var duration = endTime - startTime;
    gs.info('Before Query BR execution time (with cache): ' + duration + 'ms');
})(current, previous);

You should see a dramatic reduction in execution time after the first query.

Common Pitfalls to Avoid

1. Caching Across Scopes

Session data can behave differently across scoped applications. When using putClientData()/getClientData() inside a scoped application, the value might not be accessible from Global scope or other scoped applications, even though the session ID is the same.

Solution: Keep caching logic within the same scope or use global Script Includes for caching utilities.

2. Over-Caching

Excessive use of session caching can cause memory issues and instance performance degradation.

Solution: Only cache what you need, monitor memory usage, and implement cache size limits.

3. Caching Sensitive Data

Be cautious about caching sensitive information in session memory.

Solution: Never cache passwords, tokens, or other sensitive credentials. If you must cache sensitive data, ensure it's encrypted and properly cleared.

4. Forgetting to Clear Cache on Data Changes

If underlying data changes but the cache isn't cleared, users will see stale data.

Solution: Implement cache invalidation logic in After Update Business Rules for related tables.

Conclusion

Session caching with Script Includes and Before Query Business Rules is a powerful technique for optimizing ServiceNow performance. By implementing the patterns and best practices outlined in this guide, you can:

  • Reduce database queries by caching frequently-accessed data
  • Improve transaction processing time by 55-65%
  • Create reusable caching utilities with Script Includes
  • Optimize Before Query Business Rules that execute frequently
  • Enhance user experience with faster page loads and list views

Remember the key principles:

  • Only cache immutable data types (strings and numbers)
  • Keep total cached data under 1MB per node
  • Cache data that's accessed frequently but changes rarely
  • Implement proper cache invalidation
  • Monitor performance improvements

Start by identifying your most frequently-executed Before Query Business Rules and the data they repeatedly query. Implement session caching using the SessionCacheUtil Script Include, measure the performance gains, and iterate. Your users—and your database server—will thank you.

Happy caching!

Was this article helpful?