Use LockService to Prevent Race Conditions in Apps Script

If multiple users can trigger your script at the same time — or if you have overlapping time-based triggers — you can end up with race conditions: two executions reading and writing data simultaneously, causing duplicates or corruption.

Apps Script's LockService solves this by letting you acquire a lock before critical sections of code.

The problem: duplicate writes without a lock

Imagine a script that assigns sequential ticket numbers:

// ❌ UNSAFE — two simultaneous executions could assign the same number function assignTicketNumber() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); const lastRow = sheet.getLastRow(); const lastNumber = sheet.getRange(lastRow, 1).getValue(); const newNumber = (parseInt(lastNumber) || 0) + 1; sheet.appendRow([newNumber, new Date(), Session.getActiveUser().getEmail()]); }

If two users run this at the same time, both could read the same lastNumber and write the same newNumber.

The fix: acquire a lock first

// ✅ SAFE — LockService prevents concurrent execution function assignTicketNumberSafe() { const lock = LockService.getScriptLock(); try { lock.waitLock(10000); // Wait up to 10 seconds to acquire the lock const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); const lastRow = sheet.getLastRow(); const lastNumber = sheet.getRange(lastRow, 1).getValue(); const newNumber = (parseInt(lastNumber) || 0) + 1; sheet.appendRow([newNumber, new Date(), Session.getActiveUser().getEmail()]); Logger.log('Assigned ticket number: ' + newNumber); } catch (e) { Logger.log('Could not acquire lock: ' + e.message); throw new Error('Another process is running. Please try again in a moment.'); } finally { lock.releaseLock(); } }

Types of locks

Apps Script provides three lock types:

// Blocks ALL users and executions of this script const scriptLock = LockService.getScriptLock(); // Blocks only the CURRENT USER's concurrent executions const userLock = LockService.getUserLock(); // Blocks all executions on the CURRENT DOCUMENT const documentLock = LockService.getDocumentLock();

Use getScriptLock() when protecting shared resources like a spreadsheet. Use getUserLock() when protecting per-user state.

Check if a lock is available without waiting

function tryLockExample() { const lock = LockService.getScriptLock(); if (lock.tryLock(5000)) { // Try for up to 5 seconds try { // Do your critical work here Logger.log('Lock acquired, doing work...'); } finally { lock.releaseLock(); } } else { Logger.log('Could not acquire lock — another process is running.'); } }

Protect a form submit handler

Form submissions can come in simultaneously. Protect your onFormSubmit handler:

function onFormSubmit(e) { const lock = LockService.getScriptLock(); lock.waitLock(30000); try { const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); const row = e.range.getRow(); // Generate a unique reference number const refNumber = 'REF-' + Date.now(); sheet.getRange(row, 6).setValue(refNumber); GmailApp.sendEmail( e.namedValues['Email'][0], 'Your submission was received', `Reference number: ${refNumber}` ); } finally { lock.releaseLock(); } }

Important notes

  • waitLock(ms) throws an error if the lock can't be acquired within the timeout — always wrap in try/finally and call releaseLock() in finally.
  • Locks are automatically released when the script execution ends, even if you forget to call releaseLock().
  • The maximum lock wait timeout is 30 seconds. Don't hold a lock longer than necessary.
  • Locks are not needed for read-only operations — only when reading and then writing based on what you read.

When to use LockService

ScenarioLock needed?
Reading dataNo
Writing to a shared sheet from multiple usersYes
Sequential ID / counter generationYes
Sending emails in a loopNo
Updating a single cell based on its current valueYes
Appending rows from a form submit triggerYes