Reading mode
Same article — pick your depth.

In plain English

NetSuite doesn’t let a script run forever. Every script gets a budget of governance units — think of it as a fuel tank. Cheap operations sip fuel; expensive ones (like loading a whole record) gulp it. Run the tank dry and NetSuite kills your script mid-run, often leaving a half-finished mess. So fast SuiteScript isn’t about clever code — it’s about spending that budget wisely.

The same task, cheap vs expensive
READ ONE FIELDrecord.load10 unitssearch.lookupFields1 unit← 10× cheaperUPDATE ONE FIELDload 10+ save 20load + save30 unitsrecord.submitFields10 units← 3× cheaper

Reading one field by loading the whole record costs 10 units; a targeted lookupFields costs just 1. Updating a field with a full load-and-save costs ~30; submitFields does it for 10. Same result, a fraction of the fuel.

So why do scripts crash?

Because a script quietly spends more than its budget. A User Event script gets 1,000 units; a Scheduled script gets 10,000. Load a record inside a loop that runs a few thousand times and you’ll sail past that limit — NetSuite stops the script with a usage limit exceeded error, and whatever it was doing is abandoned. The fix is almost never “write cleverer code.” It’s “use a cheaper call, and don’t repeat it needlessly.”

Map/Reduce to the rescue

Some jobs are just big — update 50,000 orders, say. No single script has the budget for that in one run. Map/Reduce is NetSuite’s answer: it breaks the work into many small pieces, hands each piece its own fresh budget, runs them in parallel, and pauses (“yields”) automatically when it gets close to a limit. Big job, no crash.

One giant script vs Map/Reduce
10,000-unit limit✕1 Scheduled scriptoverbudgetMap/Reduce — many small jobs✓ fresh budget per stage

One script trying to do everything blows past the limit and is killed (left). Map/Reduce splits the same work into many small jobs that each stay well under budget (right).

The key patterns, in plain English

The expert version has the code and the exact numbers. The short story is seven habits:

1

Use the lightest call

Read with lookupFields, update with submitFields. Don’t load a whole record for one field.

2

Never load in a loop

Loading or searching inside a loop is the fastest way to blow the budget. Read once, outside.

3

Page big searches

runPaged pulls results in pages instead of all at once, so large jobs don’t fall over.

4

Cache reference data

Look the tax code or rate up once and reuse it — don’t re-fetch the same value every record.

5

Log strategically

Chatty logging is slow. Log decisions and errors, not every line of a loop.

6

Map/Reduce big jobs

It splits work into chunks, each with a fresh budget, and yields automatically — built for scale.

7

Watch the gauge

Check remaining units with getRemainingUsage() and stop cleanly before you crash.

What to remember

  • Governance is a budget, not a suggestion — run out and NetSuite kills your script mid-run.
  • The cheapest fix is almost always a lighter API call (lookupFields / submitFields), not clever code.
  • For anything big, Map/Reduce is the answer — it chunks the work and refreshes the budget per piece.
Want the deep technical version? Tap Expert mode at the top — all ten patterns with code, exact unit costs, async/await, live governance monitoring, and graceful yielding.
Talk to a SuiteScript engineer

Quick summary: SuiteScript performance comes down to one principle — respect governance, then optimize the hot path. This article covers the 10 patterns we apply on every NetSuite engagement: replacing record.load with search.lookupFields, paginating searches, caching reference data, batching with Map/Reduce, monitoring remaining units in real time, and a handful of anti-patterns that quietly destroy performance at scale.

Over the past eight years, our team has tuned SuiteScript implementations for ecommerce, wholesale, and B2B retail businesses processing tens of thousands of orders per day. The patterns below are not theoretical — every one comes from a real production incident, a real governance failure, or a real customer complaint about NetSuite "feeling slow."

Why governance matters more than clever code

NetSuite enforces a usage-unit budget on every SuiteScript execution. Each API call costs a fixed number of units — record.load is 10, search.lookupFields is 1, record.submitFields is 10, an external https.request is 10. Each script type has a total budget: User Event scripts get 1,000 units, Scheduled scripts get 10,000, and Map/Reduce scripts get a budget per stage.

Exceeding the budget terminates the script with SSS_USAGE_LIMIT_EXCEEDED. There is no graceful retry — the work in flight is lost unless you wrote it to be resumable. So performance optimization in SuiteScript is not just about user-perceived speed. It is about whether your script finishes at all.

Mental model: Treat every governance unit like a dollar from a fixed budget. Spend on the work that matters, batch the rest, and reserve a buffer for the unexpected. Most performance bugs we fix are not slow code — they are scripts that ran out of budget halfway through.

Pattern 1 — Use search.lookupFields instead of record.load

This is the single highest-leverage change you can make to an existing codebase. record.load instantiates the entire record in memory — all body fields, every sublist, every dependent calculation — and costs 10 units. search.lookupFields retrieves only the fields you name, costs 1 unit, and is dramatically faster.

// ❌ Slow and expensive — 10 governance units
const customer = record.load({ type: 'customer', id: customerId });
const email    = customer.getValue('email');
const tier     = customer.getValue('custentity_tier');

// ✅ Fast and cheap — 1 governance unit
const fields = search.lookupFields({
  type:    'customer',
  id:      customerId,
  columns: ['email', 'custentity_tier']
});
const email = fields.email;
const tier  = fields.custentity_tier;

Use record.load only when you actually need to mutate the record or read sublists. For everything else — read-only field access, validations, integration lookups — lookupFields wins.

Pattern 2 — Use record.submitFields instead of full save

The same logic applies to writes. If you are updating two or three body fields, do not load the record, mutate it, and call record.save (a 20-unit round trip). Use record.submitFields:

// ✅ One operation, 10 units, no record instantiation
record.submitFields({
  type:   record.Type.SALES_ORDER,
  id:     orderId,
  values: {
    custbody_processed:    true,
    custbody_processed_on: new Date()
  }
});

The catch: submitFields does not fire workflows or User Event scripts on most record types. If your business logic depends on those triggers firing, you must use the full load/save path. Read NetSuite's per-record-type documentation before assuming.

Pattern 3 — Use search.runPaged for large result sets

Calling .run().each() on a search caps you at 4,000 results. For anything larger — and in any production NetSuite account, that's most of your useful searches — use runPaged:

const pagedResults = search.create({
  type:    search.Type.SALES_ORDER,
  filters: [['status', 'anyof', 'SalesOrd:B'], 'AND', ['mainline', 'is', 'T']],
  columns: ['internalid', 'tranid', 'total']
}).runPaged({ pageSize: 1000 });

pagedResults.pageRanges.forEach(range => {
  const page = pagedResults.fetch({ index: range.index });
  page.data.forEach(result => {
    // process result
  });
});

Two things to know: pagination governance is cheap (the search itself is 10 units, regardless of how many pages you fetch), and you can break out of the loop the moment you have enough data instead of pulling the full result set.

Pattern 4 — Cache reference data

If your script looks up the same subsidiary, tax code, or configuration value 200 times in a single execution, you are wasting 199 governance units. Cache it.

const cache = {};

const getSubsidiary = (id) => {
  if (!cache[id]) {
    cache[id] = search.lookupFields({
      type:    'subsidiary',
      id:      id,
      columns: ['name', 'currency', 'country']
    });
  }
  return cache[id];
};

For data shared across script executions — currency rates, tax tables, configuration records — use the N/cache module. It survives between executions within a session-bounded window and saves units on every subsequent invocation.

Pattern 5 — Never load records inside loops

This is the most common SuiteScript performance failure we see in audits. A scheduled script processes 800 orders. For each order, it calls record.load on the customer (10 units) and the location (10 units). That is 16,000 units consumed before any actual work — and the script's budget is 10,000.

// ❌ This crashes at order ~500 with SSS_USAGE_LIMIT_EXCEEDED
orders.forEach(orderId => {
  const order    = record.load({ type: 'salesorder', id: orderId });
  const customer = record.load({ type: 'customer', id: order.getValue('entity') });
  // ... governance budget is gone
});

// ✅ Pull all needed data in one batched search instead
const orderData = search.create({
  type:    'salesorder',
  filters: [['internalid', 'anyof', orderIds]],
  columns: ['internalid', 'entity', 'entity.email', 'entity.custentity_tier', 'location']
}).run().getRange({ start: 0, end: 1000 });

Joined columns (entity.email, entity.custentity_tier) let you pull related-record fields in a single search — no separate lookups required.

Pattern 6 — Strategic, not chatty, logging

Every log.debug call inside a 10,000-iteration loop adds measurable execution time. Log at meaningful checkpoints, not every iteration.

// ❌ Logs 10,000 lines, slows the script noticeably
results.forEach((result, i) => {
  log.debug('Processing', result.id);
  // ...
});

// ✅ Progress checkpoint every 100 iterations
results.forEach((result, i) => {
  if (i % 100 === 0) {
    log.audit('Progress', `${i} / ${results.length} processed`);
  }
  // ...
});

Use log.audit for events you want surfaced in production execution logs, log.debug for development-only detail, and log.error for actual failures. Misusing levels makes production triage painful.

Pattern 7 — Use async/await for I/O-bound work in SuiteScript 2.1

If you are still on SuiteScript 2.0, this pattern is not available — see our SuiteScript 1.0 to 2.1 migration guide for how to upgrade. In 2.1, async/await dramatically improves wall-clock time for parallel I/O:

/**
 * @NApiVersion 2.1
 */

// ❌ Sequential — total time = sum of all calls
for (const id of orderIds) {
  const o = await record.load.promise({ type: 'salesorder', id });
  // ...
}

// ✅ Parallel — total time = slowest single call
const orders = await Promise.all(
  orderIds.map(id => record.load.promise({ type: 'salesorder', id }))
);
Important governance note: Parallelism does not reduce unit consumption. 50 parallel record.load.promise calls cost the same 500 units as 50 sequential ones. async/await wins on latency, not on governance.

Pattern 8 — Map/Reduce for any heavy job

Map/Reduce is the right tool whenever you process more than a few hundred records, need parallel processing, or risk hitting a script-type budget. The framework handles yielding, rescheduling, and parallel queue allocation for you.

Each Map/Reduce stage has its own governance budget — 10,000 for getInputData, 1,000 per map invocation, 5,000 per reduce invocation, 10,000 for summarize. Multiplied across hundreds of map invocations, that is orders of magnitude more total budget than any other script type.

/**
 * @NApiVersion 2.1
 * @NScriptType MapReduceScript
 */
define(['N/search', 'N/record'], (search, record) => ({

  getInputData: () => search.create({
    type:    'salesorder',
    filters: [['status', 'anyof', 'SalesOrd:B']],
    columns: ['internalid']
  }),

  map: (context) => {
    const result = JSON.parse(context.value);
    record.submitFields({
      type:   'salesorder',
      id:     result.id,
      values: { custbody_reviewed: true }
    });
  },

  summarize: (summary) => {
    log.audit('Run complete', {
      processed: summary.mapSummary.keys.iterator().each(() => true)
    });
  }
}));

One thing to watch: Map/Reduce keys must be under 3,000 characters and values under 10MB. Long composite keys or large serialized objects will throw KEY_LENGTH_IS_OVER_3000_BYTES or VALUE_LENGTH_IS_OVER_10_MB errors mid-run.

Pattern 9 — Monitor remaining governance in real time

Long-running scripts should never blindly assume they have budget left. Check before each significant operation:

const script = runtime.getCurrentScript();

results.forEach(result => {
  if (script.getRemainingUsage() < 200) {
    log.audit('Low governance', 'Rescheduling for continuation');
    task.create({
      taskType:     task.TaskType.SCHEDULED_SCRIPT,
      scriptId:     script.id,
      deploymentId: script.deploymentId
    }).submit();
    return;  // Stop processing this run
  }

  // ... normal work
});

The 200-unit threshold is a buffer — enough to log progress, persist state to a custom record, and submit the continuation task without hitting the limit mid-cleanup.

Pattern 10 — Yield gracefully, never crash

The graceful counterpart to monitoring: when you detect low governance, persist your progress to a custom record before submitting the continuation task. The next run reads that record and resumes from where you left off — no work lost, no duplicate processing.

const checkpoint = (lastProcessedId) => {
  record.submitFields({
    type:   'customrecord_script_checkpoint',
    id:     CHECKPOINT_RECORD_ID,
    values: {
      custrecord_last_processed_id: lastProcessedId,
      custrecord_last_run_at:       new Date()
    }
  });
};

This is the difference between a script that quietly self-recovers from governance pressure and one that wakes you up at 2am during the month-end close.

Anti-patterns we rip out on every audit

  • Searches inside loops — every iteration creates a new search; combine into one outer search instead.
  • Empty try/catch — silently swallowing errors hides production failures and makes debugging guesswork.
  • User Event scripts doing bulk work — User Events run inline on every save; bulk operations belong in Scheduled or Map/Reduce.
  • Calling external APIs without timeouts — https.request with no timeout can hang the script for 60+ seconds and burn execution-time budget without burning governance.
  • Unfiltered searches in production — searches without date or status filters slow down as the account grows; what worked at 10K records crawls at 100K.
  • Storing state in module-level variables — looks like caching but does not survive between executions; gives a false sense of optimization.

Key takeaways

  • Governance is the constraint — design for it before you optimize for clever syntax.
  • Replace record.load with search.lookupFields wherever you only need a few fields. Same for submitFields.
  • Never load records inside loops; batch with searches and joined columns instead.
  • Use Map/Reduce for any non-trivial bulk work — the per-stage budgets and built-in yielding are designed for it.
  • Monitor getRemainingUsage() in long-running scripts and yield before you crash.
  • async/await in 2.1 reduces latency, not unit consumption.

If your NetSuite account is hitting governance walls, slowing down at scale, or timing out on month-end jobs, talk to one of our engineers — we will audit your scripts and give you a real performance roadmap. You can also browse our other NetSuite engineering articles for related deep-dives.

Frequently asked questions

What are SuiteScript governance limits?

Governance limits are NetSuite's usage-unit budgets that cap how much work a single script execution can do. Every API call costs a fixed number of units — record.load is 10, search.lookupFields is 1, record.submitFields is 10 — and each script type has a total budget (User Event 1,000, Scheduled 10,000, Map/Reduce per-stage). Exceeding the budget triggers SSS_USAGE_LIMIT_EXCEEDED and terminates the script.

Why is search.lookupFields faster than record.load?

search.lookupFields costs 1 governance unit and only retrieves the specific fields you ask for. record.load costs 10 units and instantiates the entire record in memory, including all sublists. If you only need two or three fields, lookupFields is up to 10x cheaper and noticeably faster.

When should I use Map/Reduce instead of a Scheduled Script?

Use Map/Reduce whenever you process more than a few hundred records, need parallel processing, or risk hitting governance limits. Map/Reduce auto-yields and reschedules between stages, processes records in parallel across multiple queues, and gives you per-stage governance budgets that total far higher than a Scheduled Script's single 10,000-unit cap.

Does SuiteScript 2.1 async/await improve performance?

async/await improves wall-clock time for I/O-bound operations like external HTTP calls and parallel record loads, because operations run concurrently instead of sequentially. It does not reduce governance unit consumption — 50 parallel record.load.promise calls cost the same 500 units as 50 sequential ones. Use it for latency, not for governance savings.

How can I monitor remaining governance units in a SuiteScript?

Use runtime.getCurrentScript().getRemainingUsage() from the N/runtime module. Call it inside loops to decide whether to yield, reschedule, or break early. A common pattern is checking for fewer than 200 remaining units before each iteration and submitting a continuation task if the budget is low.

How to apply these patterns in NetSuite Step-by-step: the right API for each job — lookupFields, submitFields, runPaged, N/cache, Map/Reduce, getRemainingUsage — with official NetSuite documentation. Open the guide