Concurrent Writes to the Same Sheet Break: LockService and the 30-Run Ceiling
When users and triggers write the same asset, numbers collide and rows get overwritten. Guard with tryLock + finally + flush, and design for the 30-per-user concurrency ceiling.
Wrap read-modify-write operations in a lock. For append, number issuance, or status change: “read the current value → change it → write it”: two overlapping runs read the same value and one overwrites the other.
Why it matters
A reference number gets issued twice, or one of two simultaneous submissions vanishes. It’s invisible most of the time and blows up exactly when users pile in, so it’s hard to reproduce and erodes trust.
Lock types and the standard pattern
| Type | Serializes |
|---|---|
getScriptLock() | One critical section across the whole project |
getDocumentLock() | Within the same document only (can be null in standalone) |
getUserLock() | The same user’s executions only |
function issueNumber() {
const lock = LockService.getScriptLock();
if (!lock.tryLock(10000)) throw new Error("could not acquire lock");
try {
const sheet = SpreadsheetApp.getActive().getSheetByName("Counter");
const next = Number(sheet.getRange("A1").getValue()) + 1;
sheet.getRange("A1").setValue(next);
SpreadsheetApp.flush(); // commit before release
return next;
} finally {
lock.releaseLock(); // always in finally
}
}
Rules:
- Prefer
tryLock(timeoutMs)(never wait forever). - Always put
releaseLock()infinally. - Call
SpreadsheetApp.flush()before releasing to commit the write, or the next run reads a stale value. - Don’t make network calls, run long computations, or wait on a user inside the lock.
Deeper: the 30-run ceiling
Concurrent executions cap at about 30 per user. Counting LockService waiting, design for roughly 60 lossless concurrent users. Beyond that is the signal to move to an external DB or a queue. It’s safest to flag this limit during the sales/contract stage.
One line to keep: shared writes need tryLock + finally + flush; design the concurrency limit up front.
Frequently asked questions
- Why does concurrent writing to the same sheet corrupt data in GAS?
- Two overlapping runs read the same current value, so one overwrites the other — causing duplicate reference numbers or silently dropping a submission.
- What are the required rules for using LockService correctly?
- Use tryLock(timeoutMs) to acquire (never wait forever), always place releaseLock() in a finally block, and call SpreadsheetApp.flush() before releasing to commit the write.
- How many concurrent executions does GAS support and what should I design for?
- Concurrent executions cap at about 30 per user. Accounting for LockService wait time, design for roughly 60 lossless concurrent users; beyond that, move to an external DB or queue.