NetSuite SuiteScript 2.x Governance Limits — Units per Script Type, API Costs, and Map/Reduce
What are NetSuite SuiteScript 2.x governance limits - units per script type, API costs, Map/Reduce?
TL;DR
- Bottom line: Every SuiteScript API call has a governance unit cost (0-100 units). Each script type has a fixed budget (1,000 to 10,000 units). Exceed the budget and the script terminates immediately. Map/Reduce scripts handle governance automatically with per-phase limits and yielding. [src1]
- Key limit: User Event scripts get only 1,000 units — a single transaction record load (10) + save (20) + one search execution (10) already consumes 40 units, leaving 960 for everything else. [src2, src3]
- Watch out for: Record operation costs vary by record category — transaction records cost 2-5x more than custom records. A
record.save()on an Invoice costs 20 units, but on a custom record only 4 units. [src3] - Best for: Use this card to plan script architecture, estimate governance consumption, and choose the right script type for your workload.
- Authentication: N/A — governance limits apply to all SuiteScript execution contexts regardless of authentication method. [src1]
System Profile
This card covers the SuiteScript 2.x governance model as implemented in Oracle NetSuite (release 2026.1). Governance limits are a server-side resource management system that assigns a unit cost to every SuiteScript API call and enforces a maximum budget per script invocation. If a script exceeds its allowed units, NetSuite terminates execution immediately with an SSS_USAGE_LIMIT_EXCEEDED error. [src1]
SuiteScript 2.1 and 2.0 share identical governance costs and limits. SuiteScript 2.1 adds modern JavaScript syntax (ES6+: let, const, arrow functions, async/await, Promises) but does not change any governance behavior. [src6]
| Property | Value |
|---|---|
| Vendor | Oracle |
| System | Oracle NetSuite — SuiteScript 2.x Runtime |
| API Surface | SuiteScript 2.x (server-side JavaScript) |
| Current Version | SuiteScript 2.1 (release 2026.1) |
| Editions Covered | All NetSuite editions (SuiteCloud Developer license required) |
| Deployment | Cloud |
| API Docs | SuiteScript Governance and Limits |
| Status | GA — enforced on all accounts |
API Surfaces & Capabilities
SuiteScript governance applies to all server-side script types. Each type has a different unit budget reflecting its expected workload. [src2]
| Script Type | Max Units | Time Limit | Use Case | Parallel? | Auto-Yield? |
|---|---|---|---|---|---|
| Client Script | 1,000 | User-controlled | Field validation, UI automation | No | No |
| User Event Script | 1,000 | 10 min | Record-level beforeLoad/beforeSubmit/afterSubmit | No | No |
| Suitelet | 1,000 | 10 min | Custom UI pages, internal tools | No | No |
| Portlet Script | 1,000 | 10 min | Dashboard portlets | No | No |
| Workflow Action Script | 1,000 | 10 min | Custom workflow actions | No | No |
| Mass Update Script | 1,000 | Per record | Bulk record updates | No | No |
| RESTlet | 5,000 | 5 min | External API endpoints | No | No |
| Scheduled Script | 10,000 | 60 min | Background processing, batch jobs | No | Manual |
| Map/Reduce Script | Per-phase | Per-phase | Large-scale data processing | Yes | Automatic |
| Bundle Installation Script | 10,000 | — | SuiteApp installation logic | No | No |
| SDF Installation Script | 10,000 | — | SuiteCloud project deployment | No | No |
| Custom Plug-in | 10,000 | — | Extensibility points | No | No |
Rate Limits & Quotas
Per-API Governance Unit Costs — Key Operations
Costs vary by record type: transaction records cost the most, custom records the least. [src3]
| API Method | Transaction Records | Standard Non-Transaction | Custom Records | Notes |
|---|---|---|---|---|
record.create() | 10 | 5 | 2 | Creates in-memory record object |
record.load() | 10 | 5 | 2 | Loads existing record from DB |
record.copy() | 10 | 5 | 2 | Copies existing record |
record.transform() | 10 | 5 | 2 | Transforms record type (e.g., SO to IF) |
record.save() | 20 | 10 | 4 | Commits record to database |
record.delete() | 20 | 10 | 4 | Deletes record from database |
record.submitFields() | 10 | 5 | 2 | Inline field update without full record load |
record.attach() / detach() | 10 | 10 | 10 | Attach/detach records |
Search & Query Costs
| API Method | Units | Notes |
|---|---|---|
search.create() | 0 | Creating a search object is free |
search.load() | 5 | Loading a saved search |
search.save() | 5 | Saving a search definition |
search.lookupFields() | 1 | Cheapest way to read field values |
ResultSet.each() | 10 | Running search results iteration |
ResultSet.getRange() | 10 | Getting a page of results |
Search.runPaged() | 5 | Paginated search execution |
PagedData.fetch() | 5 | Fetching a page from paged results |
query.runSuiteQL() | 10 | Running a SuiteQL query |
HTTP/HTTPS & External Call Costs
| API Method | Units | Notes |
|---|---|---|
http.get() / post() / put() / delete() | 10 | Each external HTTP call |
https.get() / post() / put() / delete() | 10 | Each secure HTTP call |
https.requestRestlet() | 10 | RESTlet-to-RESTlet call |
https.requestSuiteTalkRest() | 10 | SuiteTalk REST call from SuiteScript |
sftp.Connection.download() | 100 | SFTP file download |
sftp.Connection.upload() | 100 | SFTP file upload |
Other High-Cost Operations
| API Method | Units | Notes |
|---|---|---|
email.send() | 20 | Sending an email |
file.save() | 20 | Saving file to File Cabinet |
file.load() | 10 | Loading file from File Cabinet |
workflow.initiate() | 20 | Starting a workflow |
task.CsvImportTask.submit() | 100 | Submitting a CSV import |
llm.generateText() | 100 | AI text generation (N/llm module) |
llm.embed() | 50 | AI embedding generation |
documentCapture.documentToText() | 100 | OCR document processing |
Map/Reduce Script — Per-Phase Hard Limits
| Phase | Hard Limit (Units) | Hard Limit (Time) | Hard Limit (Instructions) | What Happens on Exceed |
|---|---|---|---|---|
| getInputData | 10,000 | 60 min | 1 billion | Ends invocation, skips to summarize |
| map | 1,000 | 5 min | 100 million | Ends current invocation; pending jobs cancel |
| reduce | 5,000 | 15 min | 100 million | Ends current invocation |
| summarize | 10,000 | 60 min | 1 billion | Script stops executing |
[src4]
Map/Reduce Soft Limits (Auto-Yield)
| Soft Limit | Default | Configurable? | Applies To | Behavior |
|---|---|---|---|---|
| Units per job | 10,000 | No | map, reduce | Job yields and reschedules with same priority |
| Yield After Minutes | 60 min | Yes (3-60 min) | map, reduce | Job yields after time limit; configurable on deployment record |
Map/Reduce Data Limits
| Limit Type | Value | Error Code |
|---|---|---|
| Total persisted data | 200 MB | PERSISTED_DATA_LIMIT_FOR_MAPREDUCE_SCRIPT_EXCEEDED |
| Max key length | 3,000 characters | KEY_LENGTH_IS_OVER_3000_BYTES |
| Max value size | 10 MB per entry | VALUE_LENGTH_IS_OVER_10_MB |
Authentication
Governance limits apply regardless of how the script is triggered. [src1]
| Script Trigger | Auth Context | Governance Budget |
|---|---|---|
| User action (record save) | Session-based (logged-in user) | User Event: 1,000 units |
| RESTlet call via TBA | Token-Based Authentication | RESTlet: 5,000 units |
| RESTlet call via OAuth 2.0 | OAuth 2.0 M2M | RESTlet: 5,000 units |
| Scheduled job | System/administrator context | Scheduled: 10,000 units |
| Map/Reduce job | System/administrator context | Per-phase limits |
| Workflow action | Workflow executor role | Workflow Action: 1,000 units |
Authentication Gotchas
- RESTlet scripts run with the permissions of the authenticated user (TBA or OAuth 2.0), but governance limits are always 5,000 units regardless of user role. [src1]
- A User Event script triggered by a SuiteTalk REST/SOAP API call has the same 1,000-unit budget as one triggered by a user clicking Save in the UI. [src1]
- Map/Reduce scripts always run under the administrator context of the script owner — changing the deployment's "Execute As" role does not affect governance limits. [src4]
Constraints
- Per-invocation termination: When a script exceeds its governance budget, NetSuite throws
SSS_USAGE_LIMIT_EXCEEDEDand terminates execution immediately. There is no grace period and no way to catch this exception. [src1] - No limit increases: NetSuite governance limits cannot be raised through support tickets, licensing, or configuration. The only escape valve is choosing a script type with a higher budget or using Map/Reduce. [src1]
- Record type cost multiplier: Transaction records cost 2-5x more than custom records for the same operation. A
record.save()costs 20 units on a transaction record but only 4 on a custom record. [src3] - HTTP calls at 10 units each: External integrations in User Event scripts are severely constrained — 100 HTTP calls would exhaust the entire 1,000-unit budget. [src3]
- Map/Reduce 200 MB data cap: The total persisted data cannot exceed 200 MB. Exceeding this skips straight to the summarize stage. [src4]
- SuiteScript Debugger cap: The debugger imposes a 1,000-unit limit regardless of script type. [src2]
Integration Pattern Decision Tree
START — Need to process records with SuiteScript
+-- How many records?
| +-- < 50 records, triggered by user save
| | +-- Use User Event Script (1,000 units)
| | +-- Prefer search.lookupFields (1 unit) over record.load (5-10 units)
| +-- 50-5,000 records, on a schedule
| | +-- Use Scheduled Script (10,000 units)
| | +-- Implement yield logic: check getRemainingUsage() and reschedule
| +-- > 5,000 records, parallelizable
| | +-- Use Map/Reduce Script (auto-governed)
| | +-- Set Concurrency Limit on deployment record (default: 2)
| +-- External API integration
| +-- Low volume (< 100 calls)?
| | +-- RESTlet (5,000 units) for inbound API endpoint
| +-- High volume (> 100 calls)?
| +-- Map/Reduce or Scheduled Script for higher budgets
+-- Need custom UI?
| +-- Suitelet (1,000 units) — delegate heavy ops via task.submit()
+-- Error tolerance?
+-- Must complete all records: Map/Reduce (automatic retry)
+-- Best-effort: User Event afterSubmit (fire-and-forget)
Quick Reference
Governance Budget Calculator — Common Patterns
| Pattern | Operations | Unit Cost | Fits In |
|---|---|---|---|
| Simple field validation | 5x search.lookupFields | 5 | Client (1,000), User Event (1,000) |
| Load + modify + save 1 transaction record | record.load + record.save | 30 | User Event (1,000) |
| Load + modify + save 1 custom record | record.load + record.save | 6 | User Event (1,000) |
| Search + update 10 transaction records | 1x ResultSet.each + 10x record.submitFields | 110 | User Event (1,000) |
| Search + update 100 custom records | 1x ResultSet.each + 100x record.submitFields | 210 | User Event (1,000) |
| HTTP POST to external API | https.post | 10 | User Event (1,000, max ~100 calls) |
| Nightly sync: 200 transaction records | 200x (record.load + record.save) | 6,000 | Scheduled (10,000) |
| Nightly sync: 500 transaction records | 500x (record.load + record.save) | 15,000 | Map/Reduce only |
| Bulk CSV import | task.CsvImportTask.submit | 100 | Scheduled (10,000) |
| SFTP download + process file | sftp.download + file.load | 110 | Scheduled (10,000) |
| Send 10 emails | 10x email.send | 200 | Scheduled (10,000) |
| AI text generation | llm.generateText | 100 | Scheduled (10,000) |
Step-by-Step Integration Guide
1. Check remaining governance units at runtime
Before executing expensive operations, always check remaining units. [src1, src5]
/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
*/
define(['N/runtime', 'N/log'], (runtime, log) => {
const execute = (context) => {
const script = runtime.getCurrentScript();
log.debug('Remaining units', script.getRemainingUsage());
};
return { execute };
});
Verify: Check Execution Log for Remaining units: 10000 (for Scheduled Script).
2. Implement yield logic for Scheduled Scripts
Scheduled Scripts do NOT auto-yield. Implement manual yield logic to handle large datasets. [src5]
/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
*/
define(['N/runtime', 'N/search', 'N/record', 'N/task', 'N/log'],
(runtime, search, record, task, log) => {
const GOVERNANCE_THRESHOLD = 200;
const execute = (context) => {
const script = runtime.getCurrentScript();
let lastProcessedId = script.getParameter({ name: 'custscript_last_id' }) || 0;
const mySearch = search.create({
type: search.Type.SALES_ORDER,
filters: [
['internalidnumber', 'greaterthan', lastProcessedId],
'AND', ['mainline', 'is', 'T']
],
columns: ['internalid', 'entity', 'total']
});
let processedCount = 0;
mySearch.run().each((result) => {
if (script.getRemainingUsage() < GOVERNANCE_THRESHOLD) {
log.audit('Yielding', `Processed ${processedCount}, last ID: ${result.id}`);
const rescheduleTask = task.create({
taskType: task.TaskType.SCHEDULED_SCRIPT,
scriptId: runtime.getCurrentScript().id,
deploymentId: runtime.getCurrentScript().deploymentId,
params: { custscript_last_id: result.id }
});
rescheduleTask.submit();
return false;
}
const rec = record.load({ type: record.Type.SALES_ORDER, id: result.id });
rec.setValue({ fieldId: 'memo', value: 'Processed by batch' });
rec.save();
lastProcessedId = result.id;
processedCount++;
return true;
});
log.audit('Complete', `Processed ${processedCount} records total`);
};
return { execute };
});
Verify: Check Execution Log for yield messages and rescheduled task IDs.
3. Use Map/Reduce for automatic governance management
Map/Reduce scripts automatically yield and reschedule when governance limits are approached. [src4]
/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
*/
define(['N/search', 'N/record', 'N/log', 'N/runtime'],
(search, record, log, runtime) => {
const getInputData = () => {
return search.create({
type: search.Type.SALES_ORDER,
filters: [['mainline', 'is', 'T'], 'AND', ['status', 'anyof', 'SalesOrd:B']],
columns: ['internalid', 'entity', 'total']
});
};
const map = (context) => {
const searchResult = JSON.parse(context.value);
context.write({
key: searchResult.id,
value: { entity: searchResult.values.entity, total: searchResult.values.total }
});
};
const reduce = (context) => {
const soId = context.key;
const rec = record.load({ type: record.Type.SALES_ORDER, id: soId });
rec.setValue({ fieldId: 'memo', value: 'Processed by Map/Reduce' });
rec.save();
};
const summarize = (context) => {
context.reduceSummary.errors.iterator().each((key, error) => {
log.error('Reduce Error', `Key: ${key}, Error: ${error}`);
return true;
});
log.audit('Usage', `Remaining: ${runtime.getCurrentScript().getRemainingUsage()}`);
};
return { getInputData, map, reduce, summarize };
});
Verify: Navigate to Scripting > Script Status > Map/Reduce to monitor job progress.
4. Optimize governance consumption with search.lookupFields
search.lookupFields() costs only 1 governance unit vs record.load() at 5-10 units. [src3, src6]
// GOOD: 1 governance unit
const customerData = search.lookupFields({
type: search.Type.CUSTOMER,
id: customerId,
columns: ['companyname', 'email', 'creditlimit']
});
// BAD: 5 governance units (for non-transaction record)
// const customerRec = record.load({ type: record.Type.CUSTOMER, id: customerId });
Verify: Compare getRemainingUsage() before and after each approach.
Code Examples
JavaScript (SuiteScript 2.1): Governance-Aware Batch Processing in User Event
// Input: afterSubmit context — triggered when a Purchase Order is saved
// Output: Updates related Vendor Bill records (up to governance limit)
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*/
define(['N/search', 'N/record', 'N/runtime', 'N/log'], (search, record, runtime, log) => {
const afterSubmit = (context) => {
if (context.type !== context.UserEventType.CREATE) return;
const script = runtime.getCurrentScript();
const vendorId = context.newRecord.getValue({ fieldId: 'entity' });
// search.lookupFields = 1 unit (cheap)
const vendorData = search.lookupFields({
type: search.Type.VENDOR, id: vendorId,
columns: ['companyname', 'terms']
});
// ResultSet.getRange = 10 units
const openBills = search.create({
type: search.Type.VENDOR_BILL,
filters: [['entity', 'is', vendorId], 'AND', ['mainline', 'is', 'T']],
columns: ['internalid', 'total', 'duedate']
}).run().getRange({ start: 0, end: 20 });
for (const bill of openBills) {
if (script.getRemainingUsage() < 50) {
log.audit('Governance limit approaching',
`Remaining: ${script.getRemainingUsage()} units`);
break;
}
// record.submitFields on transaction = 10 units each
record.submitFields({
type: record.Type.VENDOR_BILL, id: bill.id,
values: { memo: `Linked to PO ${context.newRecord.id}` }
});
}
log.debug('Governance', `Units remaining: ${script.getRemainingUsage()}`);
};
return { afterSubmit };
});
JavaScript (SuiteScript 2.1): Map/Reduce with Email Notifications
// Input: All customers with overdue invoices (>30 days)
// Output: Email notifications sent, grouped by customer
/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
*/
define(['N/search', 'N/email', 'N/log', 'N/runtime'],
(search, email, log, runtime) => {
const getInputData = () => {
return search.create({
type: search.Type.INVOICE,
filters: [
['mainline', 'is', 'T'], 'AND',
['status', 'anyof', 'CustInvc:A'], 'AND',
['daysoverdue', 'greaterthan', 30]
],
columns: ['entity', 'tranid', 'total', 'duedate']
});
};
const map = (context) => {
// 1,000 unit budget — keep lightweight
const result = JSON.parse(context.value);
context.write({
key: result.values.entity.value,
value: { tranId: result.values.tranid, total: result.values.total }
});
};
const reduce = (context) => {
// 5,000 unit budget — email.send = 20 units
const customerId = context.key;
const invoices = context.values.map(v => JSON.parse(v));
email.send({
author: -5,
recipients: [customerId],
subject: `${invoices.length} overdue invoice(s) require attention`,
body: `You have ${invoices.length} overdue invoices.`
});
};
const summarize = (context) => {
context.reduceSummary.errors.iterator().each((key, error) => {
log.error('Reduce error', `Customer ${key}: ${error}`);
return true;
});
};
return { getInputData, map, reduce, summarize };
});
Data Mapping
Record Type Categories and Governance Cost Multipliers
| Record Category | Examples | create/load/copy/transform | save/delete | submitFields |
|---|---|---|---|---|
| Transaction | Invoice, Sales Order, Purchase Order, Cash Refund, Journal Entry | 10 units | 20 units | 10 units |
| Standard Non-Transaction | Customer, Vendor, Employee, Item, Contact | 5 units | 10 units | 5 units |
| Custom Record | Any custom record type | 2 units | 4 units | 2 units |
[src3]
Data Type Gotchas
- Transaction record operations cost 2.5x more than standard records and 5x more than custom records. A script processing 100 Sales Orders uses 3,000 units on load+save alone — exceeding the User Event budget. [src3]
search.lookupFields()at 1 unit is always cheaper thanrecord.load()at 2-10 units. Use lookupFields whenever you need fewer than ~10 fields and don't need to modify the record. [src3, src6]- The
N/cachemodule'sCache.get()costs only 1 unit. Cache frequently-accessed configuration values to avoid repeatedconfig.load()calls at 10 units each. [src3]
Error Handling & Failure Points
Common Error Codes
| Error Code | Meaning | Cause | Resolution |
|---|---|---|---|
SSS_USAGE_LIMIT_EXCEEDED | Governance units exhausted | Script consumed more units than its type allows | Refactor to reduce consumption; use higher-budget script type; use Map/Reduce |
PERSISTED_DATA_LIMIT_FOR_MAPREDUCE_SCRIPT_EXCEEDED | Map/Reduce 200 MB data cap | Too much data written between phases | Pass only IDs between phases; reload records in reduce |
KEY_LENGTH_IS_OVER_3000_BYTES | Map/Reduce key too long | Key string exceeds 3,000 characters | Use shorter keys (IDs only); pass data in values |
VALUE_LENGTH_IS_OVER_10_MB | Map/Reduce value too large | Single value exceeds 10 MB | Split data across multiple key-value pairs |
SSS_REQUEST_LIMIT_EXCEEDED | Concurrent request limit hit | Too many simultaneous SuiteScript executions | Reduce Map/Reduce concurrency; stagger scheduled scripts |
SSS_TIME_LIMIT_EXCEEDED | Script execution time exceeded | Script ran longer than its time limit | Optimize loops; reduce record count per invocation |
Failure Points in Production
- User Event afterSubmit bottleneck: An afterSubmit that calls an external API (10 units per HTTPS call) and processes 50 related records (10 units each) uses 550 units — over half the 1,000-unit budget. Fix:
Move external API calls to a Scheduled Script triggered via task.create().submit() (20 units).[src5, src6] - Map/Reduce data explosion: Emitting large JSON objects as values in the map phase can exceed the 200 MB persisted data limit. Fix:
Emit only record IDs as values in map; reload the record in reduce.[src4] - Scheduled Script runs out of units without yielding: Processing 500 transaction records (load + save = 30 units each = 15,000 total) exceeds the 10,000-unit budget. Fix:
Check getRemainingUsage() in each loop iteration and reschedule via task.create() when below threshold.[src5] - Debugger unit cap: The debugger enforces a 1,000-unit limit regardless of script type. Fix:
Use log.debug() and the Execution Log for production-scale debugging.[src2] - SFTP operations consuming 100 units each: A single sftp.download() or sftp.upload() costs 100 governance units — 10% of a Scheduled Script's budget. Fix:
Use a Map/Reduce script for multi-file SFTP operations.[src3] - Hidden governance from triggered scripts: A Map/Reduce reduce phase that calls
record.save()can trigger User Event scripts on the saved record. Those scripts have their OWN governance budget. But if they save OTHER records, those cascade further. Fix:Audit the full trigger chain; use script parameters as flags to prevent recursive processing.[src1, src4]
Anti-Patterns
Wrong: Loading full records when you only need field values
// BAD — record.load costs 5-10 units; loading 50 customers = 250-500 units
for (const custId of customerIds) {
const custRec = record.load({ type: record.Type.CUSTOMER, id: custId });
const name = custRec.getValue({ fieldId: 'companyname' });
}
Correct: Use search.lookupFields for read-only access
// GOOD — search.lookupFields costs 1 unit; 50 customers = 50 units (5-10x cheaper)
for (const custId of customerIds) {
const fields = search.lookupFields({
type: search.Type.CUSTOMER, id: custId,
columns: ['companyname', 'email']
});
}
Wrong: External API calls in User Event beforeSubmit
// BAD — blocks the user's save operation; 10 units per call; timeout risk
const beforeSubmit = (context) => {
const response = https.post({
url: 'https://external-api.example.com/validate',
body: JSON.stringify(context.newRecord.toJSON()),
headers: { 'Content-Type': 'application/json' }
}); // 10 units + blocks until response
if (response.code !== 200) throw 'Validation failed';
};
Correct: Delegate external calls to afterSubmit or Scheduled Script
// GOOD — delegate to Scheduled Script (20 units to submit)
const afterSubmit = (context) => {
if (context.type === context.UserEventType.CREATE) {
const myTask = task.create({
taskType: task.TaskType.SCHEDULED_SCRIPT,
scriptId: 'customscript_external_sync',
params: { custscript_record_id: context.newRecord.id }
});
myTask.submit(); // 20 units, async execution
}
};
[src5]
Wrong: Using record.load + record.save for simple field updates
// BAD — costs 30 units per transaction record (10 load + 20 save)
const rec = record.load({ type: record.Type.SALES_ORDER, id: soId });
rec.setValue({ fieldId: 'memo', value: 'Updated' });
rec.save();
Correct: Use record.submitFields for inline updates
// GOOD — costs 10 units for transaction record (vs 30)
record.submitFields({
type: record.Type.SALES_ORDER, id: soId,
values: { memo: 'Updated' }
});
Common Pitfalls
- Ignoring record type cost differences: Developers assume all
record.load()calls cost the same. A custom record load costs 2 units, but a transaction record load costs 10 units — 5x more. Fix:Always calculate governance budgets based on the actual record types being processed.[src3] - Not implementing yield logic in Scheduled Scripts: Map/Reduce scripts auto-yield, but Scheduled Scripts do not. Fix:
Check getRemainingUsage() in every loop iteration; reschedule via task.create().submit() when below a threshold (e.g., 200 units).[src5] - Passing large objects through Map/Reduce phases: Emitting entire record JSON objects as values in the map phase wastes the 200 MB data budget. Fix:
Pass only record IDs between phases; reload records in the reduce phase.[src4] - Stacking multiple User Event scripts on the same record: All User Event scripts deployed on the same record type and event share the same 1,000-unit budget. Fix:
Consolidate User Event scripts into a single script; or delegate heavy operations to Scheduled Script.[src1, src6] - Using config.load repeatedly:
config.load()costs 10 units each call. Fix:Cache configuration values using the N/cache module (Cache.get costs 1-2 units) with appropriate TTL.[src3] - Testing with small datasets: A script that processes 10 records using 300 units in testing will use 3,000 units processing 100 records in production. Fix:
Always test with production-volume datasets; calculate governance consumption mathematically before deployment.[src6, src7]
Diagnostic Commands
// === Add to any SuiteScript for governance monitoring ===
// Check remaining governance units for current script
const script = runtime.getCurrentScript();
log.debug('Governance Status', {
remainingUsage: script.getRemainingUsage(),
scriptId: script.id,
deploymentId: script.deploymentId
});
// Log governance usage at key checkpoints
const startUnits = script.getRemainingUsage();
// ... perform operations ...
const endUnits = script.getRemainingUsage();
log.audit('Operation cost', `Used ${startUnits - endUnits} governance units`);
// Monitor Map/Reduce job status programmatically
const mrStatus = task.checkStatus({ taskId: 'MAPREDUCETASK_12345' });
log.debug('M/R Status', {
status: mrStatus.status, // PENDING, PROCESSING, COMPLETE, FAILED
stage: mrStatus.stage, // GET_INPUT, MAP, REDUCE, SUMMARIZE
percentComplete: mrStatus.getPercentageCompleted()
});
# Check Map/Reduce job status via UI
# Navigate to: Customization > Scripting > Script Status > Map/Reduce
# Check Scheduled Script queue
# Navigate to: Customization > Scripting > Script Status > Scheduled
# View Execution Log for governance debugging
# Navigate to: Customization > Scripting > Script Execution Log
# Filter by: Script ID, Log Level, Date Range
# Look for: SSS_USAGE_LIMIT_EXCEEDED errors
Version History & Compatibility
| SuiteScript Version | NetSuite Release | Status | Key Changes | Notes |
|---|---|---|---|---|
| SuiteScript 2.1 | 2020.1+ | Current (recommended) | ES6+ syntax (let/const, arrow functions, async/await) | Same governance as 2.0 |
| SuiteScript 2.0 | 2015.2+ | Supported | Module-based architecture, AMD define() | Original 2.x version |
| SuiteScript 1.0 | Legacy | Maintenance-only | nlapiYieldScript, nlapiSetRecoveryPoint | Yield functions not in 2.x |
Recent Governance-Relevant Changes
| Release | Change | Impact |
|---|---|---|
| 2026.1 | N/llm module added (generateText: 100 units, embed: 50 units) | AI operations consume significant governance budget |
| 2025.2 | N/documentCapture module (100 units per operation) | OCR/document processing is governance-expensive |
| 2024.1 | Map/Reduce 200 MB hard limit on persisted data | Previously softer enforcement; now hard cap |
| 2023.2 | Custom Tool Scripts introduced (1,000 units) | New script type for SuiteCloud custom tools |
Deprecation Policy
Oracle NetSuite maintains backward compatibility for SuiteScript APIs across releases. SuiteScript 1.0 is in maintenance-only mode — new scripts should use 2.1. Governance unit costs for existing APIs have remained stable since SuiteScript 2.0 was introduced in 2015. [src1, src3]
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Planning which script type to use for a new customization | Sizing SuiteTalk REST/SOAP API throughput | NetSuite SuiteTalk API rate limits card |
Debugging an SSS_USAGE_LIMIT_EXCEEDED error | Configuring Token-Based Authentication | NetSuite authentication guide |
| Estimating if a script will fit within its governance budget | Understanding NetSuite licensing or editions | NetSuite edition comparison |
| Choosing between User Event, Scheduled, or Map/Reduce | Planning CSV import file size limits | NetSuite CSV import reference |
| Optimizing an existing script's governance consumption | Configuring concurrent user limits for SuiteTalk | NetSuite concurrency governance card |
Important Caveats
- Governance unit costs have been stable since SuiteScript 2.0 was introduced in 2015, but Oracle reserves the right to change costs in any release. New APIs (N/llm, N/documentCapture) were assigned costs at introduction. Always verify against the current SuiteScript 2.x API Governance page. [src3]
- The SuiteScript Debugger imposes a 1,000-unit governance cap regardless of script type. A Scheduled Script that normally uses 8,000 units cannot be fully debugged in the debugger. [src2]
- Map/Reduce scripts show "unlimited" governance in some documentation, but each phase has hard per-invocation limits (1,000-10,000 units). The "unlimited" refers to the framework's ability to automatically yield and reschedule. [src4]
- User Event scripts triggered by SuiteTalk API calls, CSV imports, or Map/Reduce record saves all share the same 1,000-unit budget. A script that works for manual user saves may fail when records are created in bulk via API. [src1, src6]
- The
getRemainingUsage()method returns remaining units for the current script invocation, not for the account or the day. There is no API to check SuiteCloud Processor queue availability. [src1]