SFMC enforces execution time limits on SSJS scripts. CloudPages typically have a 30-second timeout, and Automation Studio scripts have a 30-minute limit (but individual script activities may have shorter limits). Understanding and working within these limits is critical.

1. Minimize DE Round-Trips

Every Platform.Function.Lookup, de.Rows.Retrieve, and WSProxy call involves a network round-trip to SFMC servers. Minimize them.

Batch reads instead of per-row lookups

// SLOW — one lookup per row
for (var i = 0; i < ids.length; i++) {
    var record = Platform.Function.Lookup("DE", "data", "id", ids[i]);
    // process record
}

// FASTER — retrieve all needed rows at once
var rows = Platform.Function.LookupRows("DE", "Status", "active");
var lookupMap = {};
for (var i = 0; i < rows.length; i++) {
    lookupMap[rows[i].id] = rows[i];
}

// Then use lookupMap[id] instead of a per-row Lookup

Use LookupOrderedRows with a limit

// Get only what you need
var recent = Platform.Function.LookupOrderedRows(
    "Events", 10, "Timestamp desc",
    "UserId", userId
);

Show test script
<script runat="server">
/*
 * Chapter: Minimize DE Round-Trips
 * Proves:
 *   1. LookupOrderedRows typeof (prefer one call).
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
assert("LookupOrderedRows typeof", typeof Platform.Function.LookupOrderedRows, "clrmethodinfo");
assert("LookupRows typeof", typeof Platform.Function.LookupRows, "clrmethodinfo");
</script>

2. Cache Expensive Results

If you need to use the same DE data multiple times, load it once at the top of the script:

Platform.Load("core", "1.1.5");

// Load config once
var config = {};
var configRows = Platform.Function.LookupRows("AppConfig", "Active", "true");
for (var i = 0; i < configRows.length; i++) {
    config[configRows[i].Key] = configRows[i].Value;
}

// Use config throughout without further lookups
var timeout = config["requestTimeout"] || "30000";
var apiKey = config["apiKey"];

Show test script
<script runat="server">
/*
 * Chapter: Cache Expensive Results
 * Proves:
 *   1. In-request cache object pattern.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
var cache = {};
function getCached(key, fn) {
    if (cache[key] !== undefined) return cache[key];
    cache[key] = fn();
    return cache[key];
}
var n = 0;
getCached("a", function () { n++; return 1; });
getCached("a", function () { n++; return 1; });
assert("cache hits once", n, 1);
assert("cached value", cache.a, 1);
</script>

3. Efficient Loops

Cache array length before looping:

// CORRECT — length evaluated once
var len = items.length;
for (var i = 0; i < len; i++) {
    process(items[i]);
}

// LESS EFFICIENT — length re-evaluated each iteration
for (var i = 0; i < items.length; i++) {
    process(items[i]);
}

Break early when possible:

var found = null;
for (var i = 0; i < rows.length; i++) {
    if (rows[i].Id === targetId) {
        found = rows[i];
        break;  // stop scanning
    }
}

Show test script
<script runat="server">
/*
 * Chapter: Efficient Loops
 * Proves:
 *   1. Cached length loop.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
var rows = [1, 2, 3, 4], len = rows.length, sum = 0;
for (var i = 0; i < len; i++) sum += rows[i];
assert("sum", sum, 10);
</script>

4. HTTP Calls Are Expensive

Each HTTP request (to external APIs, SFMC REST API, etc.) adds significant latency. Minimize calls:

// BAD — multiple calls for same data
var name = callApi("/user/" + id + "/name");
var email = callApi("/user/" + id + "/email");
var prefs = callApi("/user/" + id + "/prefs");

// BETTER — single call
var user = callApi("/user/" + id);
var name = user.name;
var email = user.email;
var prefs = user.prefs;

Cache auth tokens in a DE rather than fetching a new token on every page load:

function dateAdd(timestamp,intervalToAdd,intervalType) {
    Platform.Variable.SetValue("@dateAdd_ts",timestamp);
    Platform.Variable.SetValue("@dateAdd_add",intervalToAdd);
    Platform.Variable.SetValue("@dateAdd_type",intervalType);
    return Platform.Function.TreatAsContent("%%=DateAdd(@dateAdd_ts, @dateAdd_add, @dateAdd_type)=%%");
}

function getAccessToken() {
    // Check for valid cached token
    var cached = Platform.Function.Lookup("TokenCache", "token",
        "service", "sfmcRest");
    var expiry = Platform.Function.Lookup("TokenCache", "expires",
        "service", "sfmcRest");

    if (cached && expiry && new Date(expiry) > new Date()) {
        return cached;
    }

    // Fetch new token. Platform.Function.HTTPPost only yields the HTTP status code,
    // so Script.Util.HttpRequest is required to read the token out of the body.
    var req = new Script.Util.HttpRequest(authUrl);
    req.method = "POST";
    req.contentType = "application/json";
    req.postData = Platform.Function.Stringify({ grant_type: "client_credentials",
                               client_id: clientId, client_secret: clientSecret });
    var resp = req.send();

    // statusCode is a CLR value — Number() converts it so === works
    var status = Number(resp.statusCode);
    if (status !== 200) {
        throw new Error("Token fetch failed with status " + status);
    }
    var token = Platform.Function.ParseJSON(String(resp.content) + "");

    // Cache it. DateAdd knows Y, M, D, H and MI but no seconds unit,
    // so the lifetime is rounded down to whole minutes.
    var lifetimeMinutes = Math.floor((token.expires_in - 60) / 60);
    Platform.Function.UpsertData("TokenCache",
        ["service"], ["sfmcRest"],
        ["token", "expires"],
        [token.access_token, formatDate(
            dateAdd(Platform.Function.Now(), lifetimeMinutes, "MI"),
            "MM/DD/YYYY HH:mm:ss")]
    );

    return token.access_token;
}

function formatDate(dateString,dateFormat,timeFormat,isoLocale) {
    Platform.Variable.SetValue("@formatDate_string",dateString);
    Platform.Variable.SetValue("@formatDate_date",dateFormat);
    Platform.Variable.SetValue("@formatDate_time",timeFormat);
    Platform.Variable.SetValue("@formatDate_iso",isoLocale);
    return Platform.Function.TreatAsContent("%%=FormatDate(@formatDate_string, @formatDate_date, @formatDate_time, @formatDate_iso)=%%");
}

Show test script
<script runat="server">
/*
 * Chapter: HTTP Calls Are Expensive
 * Proves:
 *   1. HttpRequest exists; avoid duplicate sends in loop (pattern).
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
assert("HttpRequest ctor", typeof Script.Util.HttpRequest === "function" || typeof Script.Util.HttpRequest === "clr" ? "ok" : typeof Script.Util.HttpRequest, "ok");
</script>

5. Automation Studio Timeout Guard

Long-running automations can time out mid-loop. Add a time guard:

var startTime = new Date().getTime();
var MAX_RUNTIME_MS = 25 * 60 * 1000; // 25 minutes

for (var i = 0; i < rows.length; i++) {
    var elapsed = new Date().getTime() - startTime;
    if (elapsed > MAX_RUNTIME_MS) {
        // Save progress marker and exit gracefully
        Platform.Function.UpsertData("ProcessingState",
            ["jobId"], [jobId],
            ["lastProcessed", "status"],
            [rows[i].id, "paused"]
        );
        break;
    }
    processRow(rows[i]);
}

Show test script
<script runat="server">
/*
 * Chapter: Automation Studio Timeout Guard
 * Proves:
 *   1. Cap processed count.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
var processed = 0, max = 10;
for (var i = 0; i < 100; i++) { processed++; if (processed >= max) break; }
assert("cap", processed, 10);
</script>

6. String Concatenation in Loops

Building large strings with += in a loop is slow for very large outputs. For large HTML generation:

// For moderate output (< a few thousand characters), += is fine
var html = "";
for (var i = 0; i < items.length; i++) {
    html += "<li>" + items[i].name + "</li>";
}
Write(html);

For very large outputs (thousands of items), use Write() directly in the loop to avoid a large string in memory:

Write("<ul>");
for (var i = 0; i < items.length; i++) {
    Write("<li>" + items[i].name + "</li>");
}
Write("</ul>");

Show test script
<script runat="server">
/*
 * Chapter: String Concatenation in Loops
 * Proves:
 *   1. join after push preferred over += in spirit — push+join works.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
var parts = [], i;
for (i = 0; i < 3; i++) parts.push("" + i);
assert("join", parts.join(","), "0,1,2");
</script>

7. Avoid Unnecessary Platform.Load

Platform.Load("core", "1.1.5") has a small overhead. Call it once per page, not multiple times:

// CORRECT — called once
Platform.Load("core", "1.1.5");

// WRONG — redundant calls add overhead
Platform.Load("core", "1.1.5");
var de = DataExtension.Init("DE1");
Platform.Load("core", "1.1.5"); // don't call again
var sub = Subscriber.Init("sub_123");

Show test script
<script runat="server">
/*
 * Chapter: Avoid Unnecessary Platform.Load
 * Proves:
 *   1. Single Load then DataExtension available.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
Platform.Load("core", "1.1.5");
assert("DataExtension", typeOfThunk(function () { return typeof DataExtension; }), "object");
</script>

8. Prefer Platform.Function for Single Lookups

For single-row lookups, Platform.Function.Lookup is faster than DataExtension.Init + Rows.Retrieve:

// Faster for single value
var email = Platform.Function.Lookup("Contacts", "Email", "Id", contactId);

// Slower for single value (Core initializes more objects)
Platform.Load("core", "1.1.5");
var de = DataExtension.Init("Contacts");
var rows = de.Rows.Retrieve({ Property: "Id", SimpleOperator: "equals", Value: contactId });
var email = rows[0] ? rows[0].Email : "";
Show test script
<script runat="server">
/*
 * Chapter: Prefer Platform.Function for Single Lookups
 * Proves:
 *   1. Lookup typeof without Core; DataExtension needs Core.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
assert("Lookup typeof", typeof Platform.Function.Lookup, "clrmethodinfo");
assert("LookupOrderedRows typeof", typeof Platform.Function.LookupOrderedRows, "clrmethodinfo");
</script>

See Also