---
# === IDENTITY ===
id: business/erp-integration/netsuite-suitescript-governance/2026
canonical_question: "What are NetSuite SuiteScript 2.x governance limits - units per script type, API costs, Map/Reduce?"
aliases:
  - "SuiteScript governance units limit per script type"
  - "NetSuite SuiteScript API usage unit costs record.load record.save search"
  - "SuiteScript Map/Reduce governance limits phases concurrency"
  - "NetSuite scheduled script governance yield remaining usage"
entity_type: erp_integration
domain: business > erp-integration > netsuite-suitescript-governance
region: global
jurisdiction: global
temporal_scope: 2025-2026

# === SYSTEM PROFILE ===
systems:
  - name: "Oracle NetSuite (SuiteScript 2.x Runtime)"
    vendor: "Oracle"
    version: "SuiteScript 2.1 / 2.0 (2026.1)"
    edition: "All editions (SuiteCloud Developer license required for custom scripts)"
    deployment: cloud
    api_surface: "SuiteScript 2.x (server-side JavaScript runtime)"

# === VERIFICATION ===
last_verified: 2026-03-01
confidence: 0.93
version: 1.0
first_published: 2026-03-01

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: volatile
  last_breaking_change: "2026.1 — N/llm module added (100 units per generateText call)"
  next_review: 2026-08-28
  change_sensitivity: high

# === CONSTRAINTS ===
constraints:
  - "Governance limits are per-script-invocation, not per-account or per-day — exceeding the limit terminates that script execution immediately"
  - "User Event scripts get only 1,000 units — a single record.load (10 units) + record.save (20 units) on a transaction record costs 30 units, leaving only 970 for all other operations"
  - "Map/Reduce scripts have per-phase hard limits (getInputData: 10,000, map: 1,000, reduce: 5,000, summarize: 10,000) — not a single pool"
  - "HTTP/HTTPS requests cost 10 units each — a User Event script can make at most 100 external API calls before exhausting its 1,000-unit budget"
  - "SuiteScript 2.1 and 2.0 have identical governance costs — 2.1 adds ES6+ syntax (async/await, arrow functions) but no governance differences"
  - "Total persisted data for a Map/Reduce script cannot exceed 200 MB — exceeds this and the script skips to summarize stage"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User needs SuiteTalk SOAP/REST API rate limits (concurrent users, request throttling)"
    use_instead: "business/erp-integration/netsuite-suitetalk-api/2026"
  - condition: "User needs NetSuite REST API (RESTful web services, not SuiteScript RESTlets)"
    use_instead: "business/erp-integration/netsuite-rest-api/2026"
  - condition: "User needs Token-Based Authentication or OAuth 2.0 setup for NetSuite"
    use_instead: "business/erp-integration/netsuite-authentication/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: script_type
    question: "Which SuiteScript script type are you using?"
    type: choice
    options:
      - "User Event (beforeLoad, beforeSubmit, afterSubmit)"
      - "Scheduled Script"
      - "Map/Reduce"
      - "Suitelet"
      - "RESTlet"
      - "Client Script"
      - "Workflow Action Script"
  - key: record_type
    question: "What type of NetSuite record are you working with?"
    type: choice
    options:
      - "Transaction record (Invoice, Sales Order, Purchase Order)"
      - "Standard non-transaction record (Customer, Vendor, Item)"
      - "Custom record"
  - key: data_volume
    question: "How many records need processing per execution?"
    type: choice
    options:
      - "< 100 records"
      - "100-10,000 records"
      - "> 10,000 records"

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/business/erp-integration/netsuite-suitescript-governance/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-03-01)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "business/erp-integration/salesforce-governor-limits/2026"
      label: "Salesforce Apex Governor Limits (cross-platform comparison)"
  solves: []
  alternative_to: []
  often_confused_with: []

# === SOURCES ===
sources:
  - id: src1
    title: "SuiteScript Governance and Limits — Oracle NetSuite Help Center"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/chapter_N3350651.html
    type: official_docs
    published: 2026-01-15
    reliability: authoritative
  - id: src2
    title: "Script Type Usage Unit Limits — Oracle NetSuite Help Center"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_N3351480.html
    type: official_docs
    published: 2026-01-15
    reliability: authoritative
  - id: src3
    title: "SuiteScript 2.x API Governance — Oracle NetSuite Help Center"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_157072844224.html
    type: official_docs
    published: 2026-01-15
    reliability: authoritative
  - id: src4
    title: "Map/Reduce Governance — Oracle NetSuite Help Center"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_4480364878.html
    type: official_docs
    published: 2026-01-15
    reliability: authoritative
  - id: src5
    title: "Governance Best Practices — Oracle NetSuite Help Center"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_164314457772.html
    type: official_docs
    published: 2026-01-15
    reliability: authoritative
  - id: src6
    title: "SuiteScript Governance and Usage Limits in NetSuite — The NetSuite Pro"
    author: The NetSuite Pro
    url: https://www.thenetsuitepro.com/netsuite-customization/netsuite-suitescript-governance-limits/
    type: technical_blog
    published: 2025-09-20
    reliability: moderate_high
  - id: src7
    title: "Governance Limitation in SuiteScript — Concentrus"
    author: Concentrus
    url: https://blog.concentrus.com/governance-limitation-in-suitescript
    type: technical_blog
    published: 2025-06-10
    reliability: moderate_high
---

# NetSuite SuiteScript 2.x Governance Limits — Units per Script Type, API Costs, and 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. The `async`/`await` syntax in 2.1 makes code easier to write but does not make scripts run faster or consume fewer governance units. [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 URL** | [SuiteScript Governance and Limits](https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/chapter_N3350651.html) |
| **Status** | GA — enforced on all accounts |

## API Surfaces & Capabilities

SuiteScript governance applies to all server-side script types. Client Scripts run in the browser but still have governance limits. The key distinction is script type — 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 |

[src2]

## Rate Limits & Quotas

### Per-API Governance Unit Costs — Key Operations

Every SuiteScript 2.x API method has a defined governance cost. Costs vary by record type category: transaction records (Invoice, Sales Order, Purchase Order) cost the most, standard non-transaction records (Customer, Item, Vendor) cost moderately, and custom records cost the least. [src3]

| API Method | Transaction Records | Standard Non-Transaction | Custom Records | Notes |
|---|---|---|---|---|
| `record.create()` | 10 | 5 | 2 | Creates in-memory record object [src3] |
| `record.load()` | 10 | 5 | 2 | Loads existing record from DB [src3] |
| `record.copy()` | 10 | 5 | 2 | Copies existing record [src3] |
| `record.transform()` | 10 | 5 | 2 | Transforms record type (e.g., SO to IF) [src3] |
| `record.save()` | 20 | 10 | 4 | Commits record to database [src3] |
| `record.delete()` | 20 | 10 | 4 | Deletes record from database [src3] |
| `record.submitFields()` | 10 | 5 | 2 | Inline field update without loading full record [src3] |
| `record.attach()` / `detach()` | 10 | 10 | 10 | Attach/detach records [src3] |

### Search & Query Costs

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

### HTTP/HTTPS & External Call Costs

| API Method | Units | Notes |
|---|---|---|
| `http.get()` / `http.post()` / `http.put()` / `http.delete()` | 10 | Each external HTTP call [src3] |
| `http.request()` | 10 | Generic HTTP request [src3] |
| `https.get()` / `https.post()` / `https.put()` / `https.delete()` | 10 | Each secure HTTP call [src3] |
| `https.request()` | 10 | Generic HTTPS request [src3] |
| `https.requestRestlet()` | 10 | RESTlet-to-RESTlet call [src3] |
| `https.requestSuiteTalkRest()` | 10 | SuiteTalk REST call from SuiteScript [src3] |
| `sftp.Connection.download()` | 100 | SFTP file download [src3] |
| `sftp.Connection.upload()` | 100 | SFTP file upload [src3] |

### Other High-Cost Operations

| API Method | Units | Notes |
|---|---|---|
| `email.send()` | 20 | Sending an email [src3] |
| `email.sendBulk()` | 10 | Bulk email send [src3] |
| `file.save()` | 20 | Saving file to File Cabinet [src3] |
| `file.delete()` | 20 | Deleting file from File Cabinet [src3] |
| `file.load()` | 10 | Loading file from File Cabinet [src3] |
| `config.load()` | 10 | Loading system configuration [src3] |
| `render.transaction()` | 10 | Rendering transaction to PDF [src3] |
| `render.xmlToPdf()` | 10 | Converting XML template to PDF [src3] |
| `workflow.initiate()` | 20 | Starting a workflow [src3] |
| `workflow.trigger()` | 20 | Triggering a workflow transition [src3] |
| `task.MapReduceScriptTask.submit()` | 20 | Submitting a Map/Reduce job [src3] |
| `task.ScheduledScriptTask.submit()` | 20 | Submitting a Scheduled Script job [src3] |
| `task.CsvImportTask.submit()` | 100 | Submitting a CSV import [src3] |
| `llm.generateText()` | 100 | AI text generation (N/llm module) [src3] |
| `llm.evaluatePrompt()` | 100 | AI prompt evaluation [src3] |
| `llm.embed()` | 50 | AI embedding generation [src3] |
| `documentCapture.documentToText()` | 100 | OCR document processing [src3] |

### Zero-Cost Operations (Free)

These operations consume 0 governance units and can be called without limit: [src3]

- All field get/set methods: `record.getValue()`, `record.setValue()`, `record.getText()`, `record.setText()`
- All sublist methods: `record.getSublistValue()`, `record.setSublistValue()`, `record.getLineCount()`
- `runtime.getCurrentScript()`, `runtime.getCurrentUser()`, `runtime.getCurrentSession()`
- `log.debug()`, `log.audit()`, `log.error()`, `log.emergency()` (limited by 60-min logging window, not units)
- `search.create()`, `search.createColumn()`, `search.createFilter()`
- `error.create()`, `format.format()`, `format.parse()`
- All `N/crypto` hashing and encryption methods (except `certificate.*` methods at 10 units each)
- All `N/xml` parsing methods
- All `N/url` resolution methods
- All `N/ui/serverWidget` form-building methods
- All `N/encode` conversion methods

### Transaction / Governor Limits — Map/Reduce Script Per-Phase

Map/Reduce scripts have separate governance budgets for each phase, with both hard limits (immediate termination) and soft limits (graceful yielding). [src4]

| 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 [src4] |
| **map** | 1,000 | 5 min | 100 million | Ends current invocation; pending jobs cancel (configurable retry) [src4] |
| **reduce** | 5,000 | 15 min | 100 million | Ends current invocation [src4] |
| **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 [src4] |
| Yield After Minutes | 60 min | Yes (3-60 min) | map, reduce | Job yields after time limit; configurable on deployment record [src4] |

### Map/Reduce Data Limits

| Limit Type | Value | Error Code |
|---|---|---|
| Total persisted data | 200 MB | `PERSISTED_DATA_LIMIT_FOR_MAPREDUCE_SCRIPT_EXCEEDED` [src4] |
| Max key length | 3,000 characters | `KEY_LENGTH_IS_OVER_3000_BYTES` [src4] |
| Max value size | 10 MB per entry | `VALUE_LENGTH_IS_OVER_10_MB` [src4] |

[src4]

## Authentication

Governance limits apply regardless of how the script is triggered. They are enforced at the SuiteScript runtime level. [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 |
| Suitelet page load | Session-based | Suitelet: 1,000 units |
| SuiteApp installation | Administrator context | Bundle Install: 10,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 or permissions. [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
<!-- Agents: read this section before recommending any integration approach from this unit.
     These are hard constraints that must not be violated. -->

- **Per-invocation termination**: When a script exceeds its governance budget, NetSuite throws `SSS_USAGE_LIMIT_EXCEEDED` and terminates execution immediately. There is no grace period, no warning, and no way to catch this exception — the script stops. [src1]
- **No limit increases**: Unlike some ERP platforms, NetSuite governance limits cannot be raised through support tickets, licensing, or configuration. The only architectural escape valve is choosing a script type with a higher budget (e.g., Scheduled Script at 10,000 units vs User Event at 1,000 units) or using Map/Reduce for automatic governance management. [src1]
- **Record type cost multiplier**: Transaction records (Invoice, Sales Order, Journal Entry) cost 2-5x more governance units than custom records for the same operation. A `record.save()` costs 20 units on a transaction record but only 4 on a custom record. Agents must factor record type into governance estimates. [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. Move external API calls to Scheduled Scripts or Map/Reduce. [src3]
- **Map/Reduce 200 MB data cap**: The total persisted data (all unmapped keys/values + unreduced keys/values + result keys/values) cannot exceed 200 MB. Exceeding this skips straight to the summarize stage. [src4]
- **SuiteScript Debugger cap**: The SuiteScript Debugger imposes a separate 1,000-unit limit regardless of script type, making it impossible to debug Scheduled Scripts that use >1,000 units. [src2]
- **Logging window**: `log.debug()` and other logging methods are free in governance units but are limited by a 60-minute logging window. Logs older than 60 minutes are purged from the Execution Log. [src3]

## 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)
|   |   +-- Keep operations lean: prefer search.lookupFields (1 unit) over record.load (5-10 units)
|   |   +-- Avoid HTTP calls if possible (10 units each)
|   +-- 50-5,000 records, on a schedule
|   |   +-- Use Scheduled Script (10,000 units)
|   |   +-- Implement yield logic: check getRemainingUsage() and reschedule
|   |   +-- Good for: nightly batch jobs, periodic syncs
|   +-- > 5,000 records, parallelizable
|   |   +-- Use Map/Reduce Script (auto-governed)
|   |   +-- Framework handles yielding, parallelism, and rescheduling
|   |   +-- Set Concurrency Limit on deployment record (default: 2)
|   |   +-- 200 MB total data cap — chunk large datasets
|   +-- External API integration
|       +-- Low volume (< 100 calls)?
|       |   +-- RESTlet (5,000 units) — inbound API endpoint
|       |   +-- User Event afterSubmit + https.post (outbound, limited)
|       +-- High volume (> 100 calls)?
|           +-- Map/Reduce or Scheduled Script — higher budgets
|           +-- Batch external calls in reduce phase (5,000 units per invocation)
+-- Need custom UI?
|   +-- Suitelet (1,000 units) — for simple pages
|   +-- For heavy Suitelet operations, delegate to Scheduled Script via task.submit()
+-- Error tolerance?
    +-- Must complete all records
    |   +-- Map/Reduce: automatic retry on failed map/reduce invocations
    |   +-- Scheduled Script: implement manual checkpointing with script parameters
    +-- Best-effort acceptable
        +-- User Event afterSubmit: fire-and-forget external calls
```

## 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: search + load + save 200 transaction records | 200x (`record.load` + `record.save`) | 6,000 | Scheduled (10,000) |
| Nightly sync: search + load + save 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) |

[src2, src3]

## Step-by-Step Integration Guide

### 1. Check remaining governance units at runtime

Before executing expensive operations, always check remaining units to avoid mid-operation termination. [src1, src5]

```javascript
/**
 * @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());
        // Returns the number of governance units remaining
        // for this script invocation
    };
    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 like Map/Reduce. You must implement yield logic manually to handle large datasets without hitting the 10,000-unit limit. [src5]

```javascript
/**
 * @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; // Reserve 200 units for rescheduling

    const execute = (context) => {
        const script = runtime.getCurrentScript();
        // Get last processed ID from script parameter (for resumption)
        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) => {
            // Check governance before each operation
            if (script.getRemainingUsage() < GOVERNANCE_THRESHOLD) {
                log.audit('Yielding', `Processed ${processedCount}, last ID: ${result.id}`);
                // Reschedule with last processed 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; // Stop iteration
            }

            // Process the record (costs 30 units for transaction load+save)
            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; // Continue iteration
        });

        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. Use this for any workload over 5,000 records or when governance budgeting is complex. [src4]

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

    // Phase 1: Get Input Data (10,000 units, 60 min)
    const getInputData = () => {
        return search.create({
            type: search.Type.SALES_ORDER,
            filters: [['mainline', 'is', 'T'], 'AND', ['status', 'anyof', 'SalesOrd:B']],
            columns: ['internalid', 'entity', 'total']
        });
    };

    // Phase 2: Map (1,000 units per invocation, 5 min)
    const map = (context) => {
        const searchResult = JSON.parse(context.value);
        // Emit key-value pairs for reduce phase
        context.write({
            key: searchResult.id,
            value: { entity: searchResult.values.entity, total: searchResult.values.total }
        });
    };

    // Phase 3: Reduce (5,000 units per invocation, 15 min)
    const reduce = (context) => {
        const soId = context.key;
        // Each reduce invocation gets fresh 5,000 units
        const rec = record.load({ type: record.Type.SALES_ORDER, id: soId });
        rec.setValue({ fieldId: 'memo', value: 'Processed by Map/Reduce' });
        rec.save(); // 30 units for transaction load + save
        log.debug('Processed', `Sales Order ${soId}`);
    };

    // Phase 4: Summarize (10,000 units, 60 min)
    const summarize = (context) => {
        log.audit('Summary', {
            inputCount: context.inputSummary.error ? 'Error' : 'OK',
            mapErrors: context.mapSummary.errors.iterator().each((key, error) => {
                log.error('Map Error', `Key: ${key}, Error: ${error}`);
                return true;
            }),
            reduceErrors: 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. Use it when you only need to read a few field values. [src3, src6]

```javascript
/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 */
define(['N/search', 'N/log'], (search, log) => {
    const beforeSubmit = (context) => {
        const customerId = context.newRecord.getValue({ fieldId: 'entity' });

        // GOOD: 1 governance unit
        const customerData = search.lookupFields({
            type: search.Type.CUSTOMER,
            id: customerId,
            columns: ['companyname', 'email', 'creditlimit']
        });
        log.debug('Customer', customerData.companyname);

        // BAD: 5 governance units (for non-transaction record)
        // const customerRec = record.load({ type: record.Type.CUSTOMER, id: customerId });
    };
    return { beforeSubmit };
});
```

**Verify**: Compare `getRemainingUsage()` before and after each approach — lookupFields uses 1 unit vs load's 5.

## Code Examples

### JavaScript (SuiteScript 2.1): Governance-Aware Batch Processing in User Event

```javascript
// 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) {
            // Check governance before each expensive operation
            if (script.getRemainingUsage() < 50) {
                log.audit('Governance limit approaching',
                    `Remaining: ${script.getRemainingUsage()} units, stopping`);
                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 Concurrency Control

```javascript
// Input:  All customers with overdue invoices
// Output: Email notifications sent, activity records created

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

    const getInputData = () => {
        // 10,000 unit budget for this phase
        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', 'email']
        });
    };

    const map = (context) => {
        // 1,000 unit budget — keep this lightweight
        const result = JSON.parse(context.value);
        // Group by customer (entity) for reduce phase
        context.write({
            key: result.values.entity.value,
            value: {
                invoiceId: result.id,
                tranId: result.values.tranid,
                total: result.values.total,
                dueDate: result.values.duedate,
                email: result.values.email
            }
        });
    };

    const reduce = (context) => {
        // 5,000 unit budget — can do more work here
        const customerId = context.key;
        const invoices = context.values.map(v => JSON.parse(v));

        // email.send = 20 units
        email.send({
            author: -5, // System user
            recipients: [customerId],
            subject: `${invoices.length} overdue invoice(s) require attention`,
            body: `You have ${invoices.length} overdue invoices totaling ${
                invoices.reduce((sum, inv) => sum + parseFloat(inv.total), 0)
            }. Please review your account.`
        });

        log.debug('Email sent', `Customer: ${customerId}, Invoices: ${invoices.length}`);
    };

    const summarize = (context) => {
        let totalSent = 0;
        context.output.iterator().each((key, value) => {
            totalSent++;
            return true;
        });
        log.audit('Collection complete', `Notifications sent: ${totalSent}`);

        // Log any errors
        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, Item Fulfillment, Journal Entry | 10 units | 20 units | 10 units |
| Standard Non-Transaction | Customer, Vendor, Employee, Item, Contact, Partner | 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. Use Map/Reduce or Scheduled Script for transaction record batch processing. [src3]
- `search.lookupFields()` at 1 unit is always cheaper than `record.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/cache` module's `Cache.get()` costs only 1 unit (or 2 if using a loader function). Cache frequently-accessed configuration values to avoid repeated `config.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 unit consumption; move to higher-budget script type; use Map/Reduce [src1] |
| `PERSISTED_DATA_LIMIT_FOR_MAPREDUCE_SCRIPT_EXCEEDED` | Map/Reduce 200 MB data cap exceeded | Too much data written between map/reduce phases | Reduce data passed between phases; use keys only for IDs, reload records in reduce [src4] |
| `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 instead [src4] |
| `VALUE_LENGTH_IS_OVER_10_MB` | Map/Reduce value too large | Single value exceeds 10 MB | Split data across multiple key-value pairs; pass only IDs and reload in reduce [src4] |
| `SSS_REQUEST_LIMIT_EXCEEDED` | Concurrent request limit hit | Too many simultaneous SuiteScript executions | Reduce Map/Reduce concurrency; stagger scheduled scripts [src1] |
| `SSS_TIME_LIMIT_EXCEEDED` | Script execution time exceeded | Script ran longer than its time limit (5-60 min depending on type) | Optimize loops; reduce record count per invocation [src2] |

### Failure Points in Production

- **User Event afterSubmit bottleneck**: An afterSubmit trigger that calls an external API (10 units per HTTPS call) and processes 50 related records (`record.submitFields` at 10 units each) uses 550 units — over half the 1,000-unit budget. Any additional User Event scripts on the same record share this budget. Fix: `Move external API calls to a Scheduled Script triggered via task.create().submit() (20 units) from the User Event.` [src5, src6]
- **Map/Reduce data explosion**: Emitting large JSON objects as values in the map phase can exceed the 200 MB persisted data limit when processing millions of records. Fix: `Emit only record IDs as values in map; reload the record in reduce where you have 5,000 units per invocation.` [src4]
- **Scheduled Script runs out of units without yielding**: Unlike Map/Reduce, Scheduled Scripts have no automatic yielding. Processing 500 transaction records (load + save = 30 units each = 15,000 total) exceeds the 10,000-unit budget. Fix: `Implement manual yield logic — check getRemainingUsage() in each loop iteration and reschedule via task.create() when below threshold.` [src5]
- **Debugger unit cap**: The SuiteScript Debugger enforces a 1,000-unit limit regardless of script type. Debugging a Scheduled Script that normally uses 8,000 units will fail. Fix: `Test with small datasets in the debugger; use log.debug() and the Execution Log for production-scale debugging.` [src2]
- **Hidden governance from triggered scripts**: A Map/Reduce reduce phase that calls `record.save()` can trigger User Event scripts on the saved record. Those User Event scripts have their OWN governance budget (separate from the Map/Reduce phase). But if those User Event scripts call `record.save()` on OTHER records, those cascade into further User Event scripts — and each level has its own 1,000-unit budget. Fix: `Audit the full trigger chain; use script parameters or custom fields as flags to prevent recursive processing.` [src1, src4]
- **SFTP operations consuming 100 units each**: A single `sftp.download()` or `sftp.upload()` costs 100 governance units — 10% of a Scheduled Script's entire budget. A script that downloads 10 files has already spent 1,000 units on SFTP alone. Fix: `Use a Map/Reduce script for multi-file SFTP operations; download one file per reduce invocation.` [src3]

## Anti-Patterns

### Wrong: Loading full records when you only need field values

```javascript
// 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' });
    const email = custRec.getValue({ fieldId: 'email' });
    log.debug('Customer', `${name}: ${email}`);
}
```

### Correct: Use search.lookupFields for read-only access

```javascript
// 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']
    });
    log.debug('Customer', `${fields.companyname}: ${fields.email}`);
}
```

[src3, src6]

### Wrong: External API calls in User Event beforeSubmit

```javascript
// 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

```javascript
// GOOD — afterSubmit doesn't block save; or better, use task.create for async
const afterSubmit = (context) => {
    if (context.type === context.UserEventType.CREATE) {
        // Option 1: Quick async (still in User Event, but non-blocking to user)
        https.post.promise({
            url: 'https://external-api.example.com/notify',
            body: JSON.stringify({ id: context.newRecord.id }),
            headers: { 'Content-Type': 'application/json' }
        });

        // Option 2: Delegate to Scheduled Script (20 units to submit)
        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

```javascript
// 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

```javascript
// 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

- **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. A script that works fine with custom records may fail with transaction records. Fix: `Always calculate governance budgets based on the actual record types being processed; use the 3-tier cost table above.` [src3]
- **Not implementing yield logic in Scheduled Scripts**: Map/Reduce scripts auto-yield, but Scheduled Scripts do not. Many developers assume Scheduled Scripts will gracefully handle large datasets — they will 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 and slows processing. Fix: `Pass only record IDs between phases; reload records in the reduce phase where you have 5,000 units.` [src4]
- **Stacking multiple User Event scripts on the same record**: All User Event scripts deployed on the same record type and event (e.g., beforeSubmit on Sales Order) share the same 1,000-unit budget. Three scripts consuming 350 units each would exceed the limit. Fix: `Consolidate User Event scripts into a single script with modular functions; or delegate heavy operations to Scheduled Script via task.create().` [src1, src6]
- **Using config.load repeatedly**: `config.load()` costs 10 units each call. Loading company preferences, accounting preferences, and general preferences in every User Event invocation wastes 30 units on configuration alone. 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 — exceeding the User Event 1,000-unit limit. Fix: `Always test with production-volume datasets; calculate governance consumption mathematically before deployment.` [src6, src7]

## Diagnostic Commands

```javascript
// === Run in SuiteScript Debugger or add to any script ===

// Check remaining governance units for current script
const script = runtime.getCurrentScript();
log.debug('Governance Status', {
    remainingUsage: script.getRemainingUsage(),
    scriptId: script.id,
    deploymentId: script.deploymentId,
    logLevel: script.logLevel
});

// 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()
});
```

```bash
# Check Map/Reduce job status via UI
# Navigate to: Customization > Scripting > Script Status > Map/Reduce
# Shows: Status, Stage, Percent Complete, Data Size, Concurrency

# Check Scheduled Script queue
# Navigate to: Customization > Scripting > Script Status > Scheduled
# Shows: Status, Queue Position, Last Run Time, Next Run Time

# 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 [src3] |
| 2025.2 | N/documentCapture module (100 units per operation) | OCR/document processing is governance-expensive [src3] |
| 2024.1 | Map/Reduce 200 MB hard limit on persisted data | Previously softer enforcement; now hard cap [src4] |
| 2023.2 | Custom Tool Scripts introduced (1,000 units) | New script type for SuiteCloud custom tools [src2] |

### 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 the introduction of SuiteScript 2.0 in 2015. New APIs (e.g., N/llm, N/documentCapture) are assigned governance costs at introduction and are documented in the API reference. [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. Use the Execution Log and strategic `log.debug()` statements for production-scale testing. [src2]
- Map/Reduce scripts show "unlimited" governance in some documentation, but this is misleading. 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 across many invocations. [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 how many SuiteCloud Processor queues are available or how many scripts are queued. [src1]

## Related Units

- [Salesforce Apex Governor Limits](/business/erp-integration/salesforce-governor-limits/2026) — cross-platform comparison of governance/governor limit models
