- 2026.07.29 What Breaks When Apps Script Projects Sprawl
- 2026.07.16 Why a Daily Apps Script Trigger Suddenly Stops
- 2026.07.15 What Breaks When Customer Data Lives in Browser Storage
- 2026.07.07 When the Browser Throws Your Data Away: The Green Checkmark Lies
- 2026.07.07 Installed ≠ Working: the Backup Trigger Never Ran Once
- 2026.07.07 GAS Has No Gradual Rollout: One 'Correct' Check Locks Out Every User
- 2026.07.07 Sheets Turns Your Text Into a Date: When '2026-06' Blocks Completion Forever
- 2026.06.18 An App Where Many Write to One Sheet: Concurrency, Duplicates, and Export Design
- 2026.06.12 Don't Read Cells One at a Time: Service Calls Are the Real Cost
- 2026.06.11 Stuffing JSON or Images into a Sheet Cell Will Break It
- 2026.06.10 One Deploy Setting Decides Your Security Model: Execute as / Access
- 2026.06.09 The 6-Minute GAS Limit: Chunk and Resume Before It Kills You
- 2026.06.08 PropertiesService Is Not a Database: the 9KB Wall
- 2026.06.07 Why Saving a Screen as an Image Freezes on Mobile
- 2026.06.06 Concurrent Writes to the Same Sheet Break: LockService and the 30-Run Ceiling You are here
- 2026.06.05 Your /exec URL Must Not Change on Every Redeploy
- 2026.06.04 External Calls Fail Sometimes: Exponential Backoff and a Retry Budget
- 2026.06.03 HtmlService Apps: Shell First, Then Async Load and Mobile Back
- 2026.06.02 When to Graduate from GAS: Limit Signals and Moving to an External DB
- 2026.06.01 Adding a Brain and an Engine Room to GAS: Vertex AI and Cloud Run
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.
If you also retry a non-idempotent write, combine the lock with an idempotency key — the lock guards against concurrent collisions, the key against retry duplicates.
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.
- 2026.07.29 What Breaks When Apps Script Projects Sprawl
- 2026.07.16 Why a Daily Apps Script Trigger Suddenly Stops
- 2026.07.15 What Breaks When Customer Data Lives in Browser Storage
- 2026.07.07 When the Browser Throws Your Data Away: The Green Checkmark Lies
- 2026.07.07 Installed ≠ Working: the Backup Trigger Never Ran Once
- 2026.07.07 GAS Has No Gradual Rollout: One 'Correct' Check Locks Out Every User
- 2026.07.07 Sheets Turns Your Text Into a Date: When '2026-06' Blocks Completion Forever
- 2026.06.18 An App Where Many Write to One Sheet: Concurrency, Duplicates, and Export Design
- 2026.06.12 Don't Read Cells One at a Time: Service Calls Are the Real Cost
- 2026.06.11 Stuffing JSON or Images into a Sheet Cell Will Break It
- 2026.06.10 One Deploy Setting Decides Your Security Model: Execute as / Access
- 2026.06.09 The 6-Minute GAS Limit: Chunk and Resume Before It Kills You
- 2026.06.08 PropertiesService Is Not a Database: the 9KB Wall
- 2026.06.07 Why Saving a Screen as an Image Freezes on Mobile
- 2026.06.06 Concurrent Writes to the Same Sheet Break: LockService and the 30-Run Ceiling You are here
- 2026.06.05 Your /exec URL Must Not Change on Every Redeploy
- 2026.06.04 External Calls Fail Sometimes: Exponential Backoff and a Retry Budget
- 2026.06.03 HtmlService Apps: Shell First, Then Async Load and Mobile Back
- 2026.06.02 When to Graduate from GAS: Limit Signals and Moving to an External DB
- 2026.06.01 Adding a Brain and an Engine Room to GAS: Vertex AI and Cloud Run