NetSuite SuiteScript 2.x Governance Limits — Units per Script Type, API Costs, and Map/Reduce

Type: ERP Integration System: Oracle NetSuite (SuiteScript 2.1 / 2.0 2026.1) Confidence: 0.93 Sources: 7 Verified: 2026-03-01 Freshness: 2026-03-01

TL;DR

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]

PropertyValue
VendorOracle
SystemOracle NetSuite — SuiteScript 2.x Runtime
API SurfaceSuiteScript 2.x (server-side JavaScript)
Current VersionSuiteScript 2.1 (release 2026.1)
Editions CoveredAll NetSuite editions (SuiteCloud Developer license required)
DeploymentCloud
API DocsSuiteScript Governance and Limits
StatusGA — 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 TypeMax UnitsTime LimitUse CaseParallel?Auto-Yield?
Client Script1,000User-controlledField validation, UI automationNoNo
User Event Script1,00010 minRecord-level beforeLoad/beforeSubmit/afterSubmitNoNo
Suitelet1,00010 minCustom UI pages, internal toolsNoNo
Portlet Script1,00010 minDashboard portletsNoNo
Workflow Action Script1,00010 minCustom workflow actionsNoNo
Mass Update Script1,000Per recordBulk record updatesNoNo
RESTlet5,0005 minExternal API endpointsNoNo
Scheduled Script10,00060 minBackground processing, batch jobsNoManual
Map/Reduce ScriptPer-phasePer-phaseLarge-scale data processingYesAutomatic
Bundle Installation Script10,000SuiteApp installation logicNoNo
SDF Installation Script10,000SuiteCloud project deploymentNoNo
Custom Plug-in10,000Extensibility pointsNoNo

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 MethodTransaction RecordsStandard Non-TransactionCustom RecordsNotes
record.create()1052Creates in-memory record object
record.load()1052Loads existing record from DB
record.copy()1052Copies existing record
record.transform()1052Transforms record type (e.g., SO to IF)
record.save()20104Commits record to database
record.delete()20104Deletes record from database
record.submitFields()1052Inline field update without full record load
record.attach() / detach()101010Attach/detach records

Search & Query Costs

API MethodUnitsNotes
search.create()0Creating a search object is free
search.load()5Loading a saved search
search.save()5Saving a search definition
search.lookupFields()1Cheapest way to read field values
ResultSet.each()10Running search results iteration
ResultSet.getRange()10Getting a page of results
Search.runPaged()5Paginated search execution
PagedData.fetch()5Fetching a page from paged results
query.runSuiteQL()10Running a SuiteQL query

HTTP/HTTPS & External Call Costs

API MethodUnitsNotes
http.get() / post() / put() / delete()10Each external HTTP call
https.get() / post() / put() / delete()10Each secure HTTP call
https.requestRestlet()10RESTlet-to-RESTlet call
https.requestSuiteTalkRest()10SuiteTalk REST call from SuiteScript
sftp.Connection.download()100SFTP file download
sftp.Connection.upload()100SFTP file upload

Other High-Cost Operations

API MethodUnitsNotes
email.send()20Sending an email
file.save()20Saving file to File Cabinet
file.load()10Loading file from File Cabinet
workflow.initiate()20Starting a workflow
task.CsvImportTask.submit()100Submitting a CSV import
llm.generateText()100AI text generation (N/llm module)
llm.embed()50AI embedding generation
documentCapture.documentToText()100OCR document processing

Map/Reduce Script — Per-Phase Hard Limits

PhaseHard Limit (Units)Hard Limit (Time)Hard Limit (Instructions)What Happens on Exceed
getInputData10,00060 min1 billionEnds invocation, skips to summarize
map1,0005 min100 millionEnds current invocation; pending jobs cancel
reduce5,00015 min100 millionEnds current invocation
summarize10,00060 min1 billionScript stops executing

[src4]

Map/Reduce Soft Limits (Auto-Yield)

Soft LimitDefaultConfigurable?Applies ToBehavior
Units per job10,000Nomap, reduceJob yields and reschedules with same priority
Yield After Minutes60 minYes (3-60 min)map, reduceJob yields after time limit; configurable on deployment record

Map/Reduce Data Limits

Limit TypeValueError Code
Total persisted data200 MBPERSISTED_DATA_LIMIT_FOR_MAPREDUCE_SCRIPT_EXCEEDED
Max key length3,000 charactersKEY_LENGTH_IS_OVER_3000_BYTES
Max value size10 MB per entryVALUE_LENGTH_IS_OVER_10_MB

Authentication

Governance limits apply regardless of how the script is triggered. [src1]

Script TriggerAuth ContextGovernance Budget
User action (record save)Session-based (logged-in user)User Event: 1,000 units
RESTlet call via TBAToken-Based AuthenticationRESTlet: 5,000 units
RESTlet call via OAuth 2.0OAuth 2.0 M2MRESTlet: 5,000 units
Scheduled jobSystem/administrator contextScheduled: 10,000 units
Map/Reduce jobSystem/administrator contextPer-phase limits
Workflow actionWorkflow executor roleWorkflow Action: 1,000 units

Authentication Gotchas

Constraints

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

PatternOperationsUnit CostFits In
Simple field validation5x search.lookupFields5Client (1,000), User Event (1,000)
Load + modify + save 1 transaction recordrecord.load + record.save30User Event (1,000)
Load + modify + save 1 custom recordrecord.load + record.save6User Event (1,000)
Search + update 10 transaction records1x ResultSet.each + 10x record.submitFields110User Event (1,000)
Search + update 100 custom records1x ResultSet.each + 100x record.submitFields210User Event (1,000)
HTTP POST to external APIhttps.post10User Event (1,000, max ~100 calls)
Nightly sync: 200 transaction records200x (record.load + record.save)6,000Scheduled (10,000)
Nightly sync: 500 transaction records500x (record.load + record.save)15,000Map/Reduce only
Bulk CSV importtask.CsvImportTask.submit100Scheduled (10,000)
SFTP download + process filesftp.download + file.load110Scheduled (10,000)
Send 10 emails10x email.send200Scheduled (10,000)
AI text generationllm.generateText100Scheduled (10,000)

[src2, src3]

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 CategoryExamplescreate/load/copy/transformsave/deletesubmitFields
TransactionInvoice, Sales Order, Purchase Order, Cash Refund, Journal Entry10 units20 units10 units
Standard Non-TransactionCustomer, Vendor, Employee, Item, Contact5 units10 units5 units
Custom RecordAny custom record type2 units4 units2 units

[src3]

Data Type Gotchas

Error Handling & Failure Points

Common Error Codes

Error CodeMeaningCauseResolution
SSS_USAGE_LIMIT_EXCEEDEDGovernance units exhaustedScript consumed more units than its type allowsRefactor to reduce consumption; use higher-budget script type; use Map/Reduce
PERSISTED_DATA_LIMIT_FOR_MAPREDUCE_SCRIPT_EXCEEDEDMap/Reduce 200 MB data capToo much data written between phasesPass only IDs between phases; reload records in reduce
KEY_LENGTH_IS_OVER_3000_BYTESMap/Reduce key too longKey string exceeds 3,000 charactersUse shorter keys (IDs only); pass data in values
VALUE_LENGTH_IS_OVER_10_MBMap/Reduce value too largeSingle value exceeds 10 MBSplit data across multiple key-value pairs
SSS_REQUEST_LIMIT_EXCEEDEDConcurrent request limit hitToo many simultaneous SuiteScript executionsReduce Map/Reduce concurrency; stagger scheduled scripts
SSS_TIME_LIMIT_EXCEEDEDScript execution time exceededScript ran longer than its time limitOptimize loops; reduce record count per invocation

Failure Points in Production

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']
    });
}

[src3, src6]

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' }
});

[src3, src6]

Common Pitfalls

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 VersionNetSuite ReleaseStatusKey ChangesNotes
SuiteScript 2.12020.1+Current (recommended)ES6+ syntax (let/const, arrow functions, async/await)Same governance as 2.0
SuiteScript 2.02015.2+SupportedModule-based architecture, AMD define()Original 2.x version
SuiteScript 1.0LegacyMaintenance-onlynlapiYieldScript, nlapiSetRecoveryPointYield functions not in 2.x

Recent Governance-Relevant Changes

ReleaseChangeImpact
2026.1N/llm module added (generateText: 100 units, embed: 50 units)AI operations consume significant governance budget
2025.2N/documentCapture module (100 units per operation)OCR/document processing is governance-expensive
2024.1Map/Reduce 200 MB hard limit on persisted dataPreviously softer enforcement; now hard cap
2023.2Custom 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 WhenDon't Use WhenUse Instead
Planning which script type to use for a new customizationSizing SuiteTalk REST/SOAP API throughputNetSuite SuiteTalk API rate limits card
Debugging an SSS_USAGE_LIMIT_EXCEEDED errorConfiguring Token-Based AuthenticationNetSuite authentication guide
Estimating if a script will fit within its governance budgetUnderstanding NetSuite licensing or editionsNetSuite edition comparison
Choosing between User Event, Scheduled, or Map/ReducePlanning CSV import file size limitsNetSuite CSV import reference
Optimizing an existing script's governance consumptionConfiguring concurrent user limits for SuiteTalkNetSuite concurrency governance card

Important Caveats

Related Units