VERSICH

Using N/cache to Improve Script Performance

using n/cache to improve script performance

Introduction

Most slow SuiteScripts are not slow because the code is bad. They are slow because the same piece of work keeps getting done over and over again.

A Map/Reduce script loads the same settings record for every one of five thousand transactions. A Suitelet runs the same saved search every time someone refreshes the page. A RESTlet logs in to the same third party API on every single call. None of these are really mistakes. They just add up, quietly, until someone notices that a job that used to finish in ten minutes now takes two hours.

The N/cache module is NetSuite's built in answer to this. It gives your script a small piece of shared memory where it can park a value and pick it up again later. Instead of fetching the same data a thousand times, you fetch it once and reuse it.

We reach for it on almost every performance tuning job we take on, and it is usually the quickest win available. This guide covers what it does, how to use it properly, and the places we most often see it go wrong.

What N/cache Actually Does

Think of the cache as a sticky note on your desk.

Someone gives you a phone number. You could dig through the directory every time you need it. Or you could write it on a sticky note and glance at that instead. The note takes a second. The directory takes a minute. Same answer either way.

N/cache is that sticky note for your script. You save a value under a name, and later you ask for it back by that same name. If it is still there, you get it instantly. If it has gone, your script fetches it fresh and writes a new note.

A few things are worth knowing before you start:

  • It only stores text. Strings and nothing else. Objects and arrays need converting first.
  • It works in server scripts only. User Event, Scheduled, Map/Reduce, Suitelet, RESTlet, Mass Update and Portlet scripts can all use it. Client scripts cannot.
  • It has been in the platform since 2016.2, so every supported account already has it.
  • The data is temporary. NetSuite is allowed to clear it whenever it wants, and it does. This matters more than most people expect, so we come back to it later.

The Problem It Solves

Here is a shape of problem we run into constantly.

A Map/Reduce script processes eight thousand sales order lines. For each line it needs to work out the right shipping cost band, and that band lives on a custom record. So the map stage loads that custom record. Eight thousand times.

The record has not changed once. It is the same three fields on every pass. But the script has no way of knowing that, so off it goes again.

That costs you in three separate places:

  1. Governance. A record load costs 5 units. A saved search costs 10. Multiply either of those by several thousand and you start hitting yield points, reschedules and hard limits.
  2. Time. Every fetch is a round trip to the database. Thousands of round trips is real wall clock time, and it is time your users are waiting on.
  3. Reliability. The longer a script runs, the more chances it has to time out, get interrupted or collide with something else on the queue.

With a cache in place, that lookup happens once. Everything after it reads from memory at a cost of 1 governance unit, instead of the 5 or 10 you were paying before. The script gets faster and cheaper at the same time, from one small change.

Setting Up a Cache

You start by asking NetSuite for a cache. If one already exists under the name you give, you get that one back. If not, NetSuite creates a new one for you.

define
(['N/cache'], function (cache) {

    var settingsCache = cache.getCache({
    name: 'versich_shipping_settings',
    scope: cache.Scope.PRIVATE

});
});

That is the whole setup. Two things to decide: a name and a scope.

Naming Your Cache

Give the cache a name that says what is inside it, and put a prefix on the front. We use the client or project name so there is no chance of colliding with a bundle or with another developer's script in the same account. The name versich_tax_config tells you far more than myCache does when you open the file again in a year.

Choosing Your Scope

Scope decides who else is allowed to read what you have stored. There are three options and the difference between them matters.

Scope

Who can read it

Reach for it when

PRIVATE

Only the script that created it. This is the default if you do not set anything.

The data belongs to one script and nothing else needs to see it. Start here unless you have a reason not to.

PROTECTED

All scripts in the same bundle. If your script is not in a bundle, then all other scripts that are also not in a bundle.

You are shipping a bundle or SuiteApp and several scripts inside it share the same reference data.

PUBLIC

Every server script in the NetSuite account.

The data is genuinely account wide and completely harmless for any script to read, such as a shared currency or tax rate table.

One rule we hold to without exception: nothing sensitive goes in a PUBLIC cache. No access tokens, no credentials, no customer data, nothing tied to a particular user's permissions. Any script in the account can read a public cache, including scripts you did not write and bundles you did not install. If you are unsure, use PRIVATE.

The Loader Pattern

This is the part that makes N/cache genuinely pleasant to work with, and it is the part most people skip.

When you ask the cache for a value, you can also hand it a function that knows how to fetch that value from scratch. NetSuite only calls that function when the value is missing. When it does, it stores the result for you automatically.

function loadShippingSettings(context) {
        var settings = search.lookupFields({
        type: 'customrecord_versich_shipping',
        id: context.key,
        columns: ['custrecord_band', 'custrecord_rate', 'custrecord_zone']
});
  return JSON.stringify(settings);
}
function getSettings(recordId) {
     var settingsCache = cache.getCache({
     name: 'versich_shipping_settings',
     scope: cache.Scope.PRIVATE
});
 var raw = settingsCache.get({
       key: recordId,
       loader: loadShippingSettings,
       ttl: 3600});

   return raw ? JSON.parse(raw) : null;
}

Now the rest of your script just calls getSettings(123) and gets an object back. It never has to know or care whether that value came out of memory or out of a fresh lookup. All of that logic sits in one place, which makes it far easier to change later.

Notice that the loader receives an object with the key on it, which is why we read context.key rather than hard coding a record ID. That small detail means one loader function can serve any number of different keys. Same function, different record, no copy and paste.

The governance side is worth spelling out. A cache hit costs 1 unit. A cache miss that runs your loader costs 2 units, plus whatever the lookup inside the loader costs. Set that against 5 units for a record load or 10 for a search, paid every single time, and the arithmetic makes the case on its own.

NetSuite's own documentation recommends the loader as the main way to fill a cache, and we agree. It handles the miss case for you, and it keeps your fetch logic in one function instead of scattered across the script.

Using put() Directly

Sometimes you already have the value in your hand and simply want to store it. That is what put() is for.

settingsCache.put({
    key: 'exchange_rate_gbp_usd',
    value: JSON.stringify(rateObject),
    ttl: 1800
});

It costs 1 governance unit. It is useful when the value came from somewhere a loader cannot easily reach, such as a figure you calculated across several records, or a token that arrived in a response you were already processing for another reason.

The catch is that put() on its own does nothing for you when the value later disappears. You still need a plan for the moment the cache comes back empty. That is the main reason we lean on the loader pattern for most work and keep put() for the cases that need it.

Storing Objects and Lists

The cache holds strings only, so anything with structure has to be converted on the way in and rebuilt on the way out.

// going in
myCache.put({ key: 'zone_map', value: JSON.stringify(zoneMap) });
// coming back out
var raw = myCache.get({ key: 'zone_map', loader: buildZoneMap });
var zoneMap = raw ? JSON.parse(raw) : {};

NetSuite will run JSON.stringify() for you if you pass something that is not a string, both in put() and on whatever your loader returns. We still prefer to do it ourselves. It makes the intent obvious to whoever reads the code next, and it stops the classic bug where someone forgets to parse on the way back out and ends up comparing an object against a string.

There are two size limits to respect. A key can be up to 4KB, which is 4,096 bytes. A value can be up to 500KB.

500KB is more room than it sounds like. A tidy lookup table of a few thousand rows fits comfortably. A full dump of your item master does not. If you find yourself anywhere near the limit, that is usually a signal that you should be caching a smaller and more targeted slice of the data rather than the whole thing.

How Long Data Stays: TTL

TTL stands for time to live. It is how long, in seconds, a value is allowed to sit in the cache before NetSuite throws it away.

myCache.get({ key: 'tax_config', loader: loadTaxConfig, ttl: 3600 });

The rules are short, but two of them are easy to misread:

  • The minimum is 300 seconds, which is five minutes. You cannot set anything lower.
  • There is no maximum, and if you leave the ttl out entirely there is no default limit applied.
  • The TTL is a ceiling, not a promise. NetSuite is free to drop your value long before the time is up, and in a busy account it regularly does.

That last point is the one that catches people out. The cache is not storage. It is a convenience. Write your script so that it still works perfectly if every single lookup misses, and the cache becomes pure upside rather than something you are quietly depending on.

For picking an actual number, we work backwards from how stale the data is allowed to be. Configuration and settings records that change once a month can sit for a few hours. Exchange rates or pricing pulled from an external service usually want 15 to 60 minutes. API access tokens should expire a little before the token itself does, so you refresh ahead of the failure rather than after it. And anything a user might edit and then immediately expect to see reflected on screen should be kept short and cleared properly, which brings us to the next section.

Clearing the Cache When Data Changes

TTL on its own is a blunt instrument. If someone updates a configuration record at nine in the morning, nobody wants to wait an hour for the change to take effect.

remove() deletes a key immediately.

myCache.remove({ key: recordId });

The pattern we like here is a small User Event script sitting on the record being cached. On afterSubmit, it removes the matching key. The next script that asks for that value gets a miss, the loader runs, and everybody is looking at current data within seconds instead of hours.

function afterSubmit(context) {
        var settingsCache = cache.getCache({
        name: 'versich_shipping_settings',
        scope: cache.Scope.PUBLIC
});
settingsCache.remove({ key: context.newRecord.id });
}

Watch the scope in that example. A PRIVATE cache belongs to the script that created it, so a separate User Event script has no way to reach in and clear it. If you want cross script invalidation like this, the cache has to be PROTECTED or PUBLIC, and you need to be comfortable with what that means for whatever is stored inside it. That trade off between convenience and exposure is a real design decision, not a formality.

Mistakes We See

Treating the Cache Like a Database

We have picked up scripts that stored work in progress in the cache and then fell over when it vanished halfway through a run. The cache is allowed to forget, without warning and without an error. Anything you cannot afford to lose belongs in a custom record.

Caching Data That Changes Constantly

Open balances, live inventory counts, order status. If the value changes minute to minute, caching it does not make your script faster in any useful sense. It makes it confidently wrong, which is worse than slow.

Not Handling a Null Return

If there is no loader function and the key is not in the cache, get() returns null. Then JSON.parse(null) hands back null, and the line after that throws an error that looks nothing like the actual cause. Always guard the parse.

Caching Things That Were Already Cheap

A single search.lookupFields() call costs 1 unit, which is exactly what a cache read costs. Wrapping it saves you nothing and gives the next developer an extra layer to read through. Save caching for the genuinely expensive work: full saved searches, record loads, external API calls, and anything at all that runs inside a loop.

Putting Sensitive Data in a Public Cache

This one is worth saying twice. Tokens, credentials and anything tied to a specific user's permissions should never sit somewhere that every script in the account can read.

Keys That Collide

If two different pieces of data end up under the same key, one silently overwrites the other and nothing tells you. Prefix your keys the same way you prefix your cache names, and include the record type where it helps.

When Not to Reach for N/cache

Caching is not free. It adds a layer, and it adds a question every future developer has to ask about whether the data they are looking at is current. Skip it when:

  • The data changes about as often as you read it.
  • You are working in a client script, where the module simply is not available.
  • You need the value to survive beyond the session or the day. Use a custom record for that.
  • The script is a one off and finishes in three seconds anyway.
  • The lookup is already trivial and only happens once per execution.

The best candidates are the exact opposite of that list: data that gets read many times, changes rarely, and costs something real to fetch. When all three are true, a cache is close to a free upgrade.

Quick Reference

The numbers below are the ones we find ourselves looking up most often, so they are worth keeping somewhere close to hand.

Detail

Value

Maximum key length

4KB (4,096 bytes)

Maximum value size

500KB

Minimum TTL

300 seconds (five minutes)

Maximum TTL

None

Default TTL if not specified

No limit applied

Governance, cache hit

1 unit

Governance, cache miss using a loader

2 units, plus the cost of the lookup itself

Governance, put()

1 unit

Value type accepted

Strings only, non strings are converted with JSON.stringify()

Supported script types

Server scripts only

Available since

2016.2

Conclusion

N/cache is one of the smallest changes you can make to a SuiteScript for one of the biggest results. There is nothing to install, no custom records to build and no bundle to deploy. It is three methods and a bit of judgement about what is safe to hold on to and for how long.

Our advice is to start narrow rather than caching everything at once. Find the single slowest repeated lookup in your worst performing script, wrap it in a cache with a loader function, and measure the difference in execution time and governance units. Once you have seen the numbers on something real, it becomes obvious where else the same treatment will pay off.

The scripts that benefit most are usually the ones nobody has looked at in a while. A Map/Reduce job that grew from five hundred records to fifty thousand. A Suitelet that started simple and picked up four more searches along the way. Those are the ones where a cache turns a two hour job back into a ten minute one.

If you would like a hand reviewing your scripts for performance, or you are dealing with Map/Reduce jobs that time out or eat through governance before they finish, our NetSuite development team can help. Get in touch with us and we will take a proper look at what is slowing you down.

Frequently Asked Questions

What is the N/cache module in NetSuite?

N/cache is a SuiteScript 2.x module that gives your script a small piece of shared memory to store values and retrieve them later. Instead of loading the same record or running the same search repeatedly, you fetch it once and reuse the stored result. It has been available since NetSuite 2016.2.

Which script types can use N/cache?

Server scripts only. User Event, Scheduled, Map/Reduce, Suitelet, RESTlet, Mass Update and Portlet scripts can all use it. Client scripts cannot access the module at all.

What is the difference between PRIVATE, PROTECTED and PUBLIC cache scope?

PRIVATE is the default and restricts access to the script that created the cache. PROTECTED allows all scripts within the same bundle to read it. PUBLIC allows every server script in the account to read it, which is why tokens, credentials and permission-sensitive data should never be stored there.

How much governance does N/cache use?

A cache hit costs 1 unit. A cache miss that runs a loader function costs 2 units plus whatever the lookup inside the loader costs. A put() call costs 1 unit. Compare that against 5 units for a record load or 10 for a saved search, paid on every execution.

What are the size limits for NetSuite cache keys and values?

A key can be up to 4KB and a value can be up to 500KB. That is generous for configuration data and lookup maps but not suitable for large search result sets. If you are approaching the limit, cache a smaller derived structure rather than the raw data.

What is the minimum TTL for a NetSuite cache?

300 seconds, or five minutes. Anything lower is treated as 300. There is no maximum, and leaving the ttl parameter out applies no limit. Bear in mind that TTL is a ceiling rather than a guarantee, since NetSuite can clear cached values at any time.

Can you store objects or arrays in N/cache?

Not directly. The cache holds strings only, so objects and arrays need converting with JSON.stringify() on the way in and JSON.parse() on the way out. NetSuite will stringify non-string values automatically, but doing it explicitly keeps the stored shape under your control.

How do you clear a NetSuite cache when the underlying data changes?

Use the remove() method to delete a specific key immediately. The usual pattern is a User Event script on the cached record that removes the matching key on afterSubmit, so the next read triggers a fresh lookup. This requires PROTECTED or PUBLIC scope, since a separate script cannot reach into a PRIVATE cache.

When should you avoid using N/cache?

Skip it when the data changes as often as you read it, when you are working in a client script, when the value needs to survive beyond the session, or when the lookup is already cheap and runs once per execution. Caching frequently changing data such as open balances or live inventory makes a script confidently wrong rather than faster.