- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
10-29-2023 08:29 PM - edited 12-24-2024 10:09 PM
When it comes to performance optimizations you will often come across the concept of "caching". But what does this mean and why is caching a good option to accelerate script execution in ServiceNow? This article provides a few insights into this exciting topic - without claiming to present everything in full.
What is Caching?
In computing, a cache is a high-speed data storage layer that holds subsets of data that are often transient in nature so that future requests for that data can be satisfied faster than accessing the data's primary storage location. Caching allows you to efficiently reuse previously retrieved or calculated data.
The data within a cache is usually stored in fast-access hardware such as RAM (Random Access Memory) and can also be used in relation to software components. The main purpose of caching is to improve data retrieval performance by reducing the need to access the underlying slower storage layer. Unlike a database, where data is typically complete and persistent, caches commonly store subsets of data temporarily.
Depending on the use case, a cache might be just a variable storing the result of an expensive database operation for later reuse up to a central infrastructure layer that can be accessed by different systems with their own life cycles and architectural topologies.
Basic storage concept of a cache
Storing data in a cache is similar to dropping off luggage at an appropriate depository. And in order for you to actually get your own luggage when you pick it up later, you need some unique reference like a number or a key. In IT, such key-value stores are called dictionaries or hashmaps, and in JavaScript, objects are perfect for this purpose.
To implement a simple cache with the help of a JavaScript object you probably will leverage the concept of the so-called "lazy loading". This is a "technique commonly used in computer programming and mostly in web design and web development to defer initialization of an object until the point at which it is needed." (see Wikipedia).
Let's take a look at a simple lazy-loading implementation:
When executing method getData() it is checked first whether there is already data available for the passed key "k1" in the variable _objCache. Since this is not the case, a GlideDateTime object is only created and stored in the cache at this moment. After waiting 2 seconds, another invocation of getData() does not return a new GlideDateTime object but the previously stored one (it has the same timestamp).
What data types should be preferred for caching?
Basically, you can store all possible data under a key within a JavaScript object. But for caching reasons you should only use immutable data types, and in JavaScript these are pure strings and numbers. All other data types are internally represented by references, and that means you don't store the content in a cache but only its reference to another memory area. This results in two negative aspects:
- The referenced data can be changed outside the cache. For example, if you put a GlideRecord instance into a cache, you can continue working with that object outside, such as loading additional records from the database. However, the consumers of the cache do not know about this and rely on the fact that the stored data remains unchanged.
- As long as active references to data structures exist in memory, these referenced data structures cannot be cleaned up and thus the occupied memory space cannot be released by the garbage collector. In the worst case (and in simple words), this can lead to the ServiceNow instance becoming slower and slower, or even crashing.
Caching in ServiceNow
The following diagram provides an overview of the different cache types in ServiceNow, which are discussed in more detail in the following chapters:
Caching in individual Scripts
For caching within individual scripts everything applies which was described in the previous chapter and normally a developer doesn't think about this when using variables to cache any values in them.
Since the sandbox in which the script was executed is completely cleared away after the script is completed, it is unproblematic in this case to store entire objects (e.g. GlideRecord instances). However it is always a good idea to set any reference variables to null if the values contained by them are not used any longer.
Caching during an HTTP Transaction
When you go to a browser and request a Web page (e.g. from ServiceNow), the sequence of events that follow can be considered an HTTP transaction. In simple terms, an HTTP transaction is separated into a single HTTP request and the corresponding HTTP response.
In ServiceNow most transactions and their technical parameters are logged at table syslog_transaction and taking a look at it shows that for most of the client-triggered transactions many Business Rules are executed. However, this type of artifact is just the most prominent one. Apart from Business Rules many different types of artifacts might be executed during a transaction like Scripted REST APIs, ACLs, Script Includes, Data Policies, Flows, Workflows, Notifications, etc. And all these script types run independently of each other without any option to store certain data in a central place. Only Business Rules can access that global g_scratchpad object, which is later sent to the client and therefore not suitable for caching on server-side.
But what if at the beginning of the processing chain a complex data structure is built which should be available for all coming scripts during the transaction?
Some time ago I was faced with a weird situation in the FSM context: When templates are applied to Work Orders during their creation many different artifacts are generated, including Work Order Tasks, which reference the Work Order via the parent field (well-known concept in the task table). When analyzing a bug, it turned out that the Work Order Tasks are created before (!!) the Work Order, but they already contained the Sys ID of the latter created Work Order in the parent field. Basically, this is impossible! After intensive code research, I found that although a GlideRecord object was created for the Work Order, it was not stored in the database using the insert() operation. Instead, the Work Order's GlideRecord object was cached in a central location using the undocumented API method
//add a value to cache
GlideController.putGlobal('Maik', 'Skoddow');
//test whether key exists
gs.info(GlideController.exists('Maik'));
//retrieve cached value
gs.info(GlideController.getGlobal('Maik'));
//remove cached value by its key and thus free memory
GlideController.removeGlobal('Maik');
Use Case: Collision/Recursion Detection
Let's assume that during a transaction, many Script Includes (or Business Rules) are fired. As they have no knowledge of each other, they also cannot know whether one of them already have been executed during the transaction. That means there is a risk of getting trapped in an endless loop or recursion.
But if all Scripts would use a centrally cached array to add any unique ID (e.g. artifact's Sys ID) they also could check whether in that list that unique ID is already available and thus the script has been executed before.
Session Caching
There is an excellent article about that topic written by the ServiceNow Technical Support Performance Team which explains in detail session caching along with measurement results and some implications: Caching data to improve performance
In short, you can use the following API methods to deal with a session cache:
//add a value to cache
gs.getSession().putClientData('Maik', 'Skoddow');
//test whether key exists
gs.info(gs.getSession().getClientData('Maik') !== null);
//retrieve cached value
gs.info(gs.getSession().getClientData('Maik'));
//remove cached value by its key and thus free memory
gs.getSession().clearClientData('Maik');
Use Case: Data Segregation
Data segregation is the act of partitioning specific datasets from other datasets in order to differentiate how each dataset is accessed. The ultimate purpose of this approach is to grant access to individuals who have authorization to view specific data.
In ServiceNow, instead of leveraging Domain Separation (too expensive, too oversized) or ACLs (bad user experience) for a clear separation of data between different tenants, customers often use before query Business Rules. With this type of Business Rules, you can add further criteria to a query before it is executed to restrict the results as per requirements.
Let's assume all configuration items in a CMDB are assigned to a certain company. Companies may have parent companies to reflect a hierarchy. A user is assigned to one of the companies within that hierarchy, and therefore may only see the configuration items of his assigned company or one of its parent companies. The list of a parent companies up to the root is a candidate for caching because determining this list for each execution of a before query Business Rule would dramatically degrade instance performance.
The following Script Include provides all required code for:
- retrieving the list of all parent companies in a hierarchy (_getHierarchyOfSysIDs()),
- caching that list for the currently logged-in and returning it to the caller (getCompaniesForCurrentUser()) and
- extending a GlideRecord query to reduce the results according to that list of companies (secureQueryByCompanies()).
var DataSegregationHelper = Class.create();
DataSegregationHelper.prototype = {
initialize: function() {
},
/**
* Traverses for a certain field `geField` its parent references up to the root
* and returns all findings as a array of Sys IDs.
*/
_getHierarchyOfSysIDs: function(geField, strParentFieldName) {
var _arrSysIDs = [];
var _strParentFieldName = String(strParentFieldName || 'parent').trim();
//make sure "geField" really represents a GlideElement field of a GlideRecord
if (typeof geField === 'object' &&
typeof geField.getED === 'function' &&
String(geField.getED().getInternalType()) === 'reference') {
while (geField.getRefRecord().isValidRecord()) {
var _strCurrentSysID = geField.getRefRecord().getValue('sys_id');
//recursion detection
if (_arrSysIDs.indexOf(_strCurrentSysID) !== -1) {
break;
}
_arrSysIDs.push(_strCurrentSysID);
//traverse up the hierarchy
geField = geField.getRefRecord().getElement(_strParentFieldName);
}
}
return _arrSysIDs;
},
/**
* Returns an cached array of all Sys IDs to company records which are in a hierarchy -
* starting with the company assigned to the currently logged-in user. In case
* no company record is assigned to the currently logged-in user a dummy value is
* returned to make sure a user has no access per default.
*/
getCompaniesForCurrentUser: function() {
var _strKey = 'companies_for_' + gs.getUserID();
var _strCompanies = gs.getSession().getClientData(_strKey) || '';
if (_strCompanies.length === 0) {
var _grUser = new GlideRecord('sys_user');
_grUser.get(gs.getUserID());
_strCompanies = (
_getHierarchyOfSysIDs(_grUser.company) ||
['00000000000000000000000000000000']
).join(',');
gs.getSession().putClientData(_strKey, _strCompanies);
}
return _strCompanies;
},
/**
* Extends a GlideRecord query with an expression that reduces the results to only
* records the currently logged-in user is allowed to see as per his assigned company.
*/
secureQueryByCompanies: function(grCurrent, strFieldName) {
var _strCompanyFieldName = strFieldName || 'company';
if (grCurrent.isValidField(_strCompanyFieldName)) {
grCurrent.addEncodedQuery(
_strCompanyFieldName + 'IN' + this.getCompaniesForCurrentUser()
);
}
},
type: 'DataSegregationHelper'
};
Now you only need a simple before query Business Rule on the cmdb_ci table to restrict the list of configuration items:
System-wide Caching
The approach for session-based caching explained in the previous chapter has a downside: if the cached information is the same for many users, then pretty much redundant data is held within the application node's memory. And also think about what happens if during the time a user is logged-in the company structure is redefined by a system administrator or users are assigned to other companies. In that case, all logged-in users would continue using the wrong data unless they renew their session via logging out and in again. For such scenarios, an even more centralized cache, which is available across sessions, would be required.
And indeed, these system-wide caches exist in large numbers and only with their help can ServiceNow ensure a reasonably satisfying performance. This is achieved by storing often used values in memory instead of pulling them over and over again from the database. As an aggravating fact, individual caches may have dependencies on other caches. A prominent example of this is the values from the sys_properties table, which are at the top of the dependency hierarchy. If the checkbox "Ignore cache" is not set, the modification of such a record from the sys_properties table results in practically all existing system caches being flushed. The associated refilling of the caches during the next transactions is quite time-consuming, which causes the system performance to drop dramatically. The support article When to use 'ignore cache' on 'sys_properties records sheds more light on the topic.
Most ServiceNow users and developers are aware of these system caches as they know the page /cache.do which flushes the entire system cache. But there is more to discover. For example with the URL /xmlstats.do?include=cache you can get a listing of all available system caches along with some statistics.
But how to create and maintain a private cache? Again, analysis of the OOTB code will help and the script Include AccountsCacheUtil from the CSM module will provide the answer. With the following code you can initialize a private cache and store a value in it. The basic principles are the same, except that here you still have to include the name of the cache additionally.
//only required once to initialize a cache!!
GlideCacheManager.addPrivateCacheable('my_private_cache');
//add a value to cache
GlideCacheManager.put('my_private_cache', 'Maik', 'Skoddow');
//test whether key exists
gs.info(GlideCacheManager.get('my_private_cache', 'Maik') === null);
//retrieve value from cache
gs.info(GlideCacheManager.get('my_private_cache', 'Maik'));
//remove cached value by its key and thus free memory
GlideCacheManager.prefixFlush('my_private_cache', 'Maik'));
//empty the complete cache and thus free memory
GlideCacheManager.flush('my_private_cache');
On the previously mentioned page /xmlstats.do?include=cache you now can find the private cache within the list:
- 11,805 Views
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Very Helpful. Great Article
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
@Maik Skoddow Thanks for sharing, nice articulation.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Great article! I have a small question: What security constraints will be present if I attempt to save an access token for an integration in the cache memory?
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Thanks @Maik Skoddow for sharing this article and detailed explanations. The /xmlstats.do?include=cache was interesting one to find.
Since Tokyo, I believe, there is ScopedCacheManager API available for scoped application, which can be utilized to manage System Definition > Scoped Caches (https://docs.servicenow.com/bundle/utah-api-reference/page/integrate/guides/scopedcachemgr/concept/s...). It could be handy for future reader, if the article updated with this information.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Great article as always Maik!
A "static" member of a Script Include Class will be evaluated once per transaction and can be accessed multiple times in a transaction. This can act like a type of global transaction caching as any script can access StaticMember. This has the added benefit of having all your code in one place.
var StaticMember = Class.create();
StaticMember.count = 0; // a static member
StaticMember.overallTime = 0; // another static member
StaticMember.prototype = {
initialize: function() {},
type:'StaticMember'
};
Also see:
Caching data to improve performance
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Hi @Mwatkins
Many thanks for this valuable tip. I didn't realize that until now!
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Insightful as usual @Maik Skoddow, thanks for exploring these topics!
I have 3 questions:
- In the /xmlstats.do?include=cache page, there are statistics, as you mentioned, for certain entries.(e.g. load_time ), but not for the private cache. Did you figure out a way to query/get this data for the private cache you created? Or does it need to be set with a method to start generating statistics?
- How can you remove the private cache permanently or it stays there indefinitely after creation?
- Would you use this or the session data to store a data object when processing data in a multi-node manner?(i.e. running a script on the nodes of the instance)
Mit freundlichen Grüßen,
Lori
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Hi,
For anyone looking for answers on similar questions, here's what I found:
- Statistics got generated for the private cache after some use, though couldn't find accessible methods to query it.
- It got removed after node restart, you can't remove it with any method call. So, make sure to always flush it to release mem allocation.
- None of the caching mechanisms work across nodes. Storing the value (e.g. in a system property) in the database worked across nodes.
In case anyone finds a different approach using either one of the caches or otherwise, do let me know.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
After initializing a cache with:
GlideCacheManager.addPrivateCacheable(cacheName);
The new cache is listed on /xmlstats.do?include=cache, which shows the usage. However, the max_entries is always set to 50. How can I set the max entries to a different value?
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Amazing article, thank you so much!

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
What is the TTL on private caches?