Apps Script example · 8 min read

Prevent Race Conditions with LockService in Apps Script: Copy-Paste Apps Script Pattern

Working prevent race conditions with lockservice in apps script example in Apps Script—copy-paste code, common mistakes, and when to get it built professionally.

LockServiceConcurrencyProperties

Apps Script functions can run concurrently, whether triggered by multiple users clicking a button at once or several trigger events firing close together, and any code that reads a shared value, changes it, and writes it back is vulnerable to two executions overlapping and losing an update.

This tutorial demonstrates the problem with a shared counter: two overlapping executions can both read the same starting value, each add one, and both write back the same result, silently dropping one of the increments.

LockService.getScriptLock() provides a lock shared across every execution of the script, and calling tryLock with a timeout makes the second execution wait its turn instead of racing the first one to read and write the counter.

The lock is always released inside a finally block, which matters because an uncaught error partway through the critical section would otherwise leave the lock held until it expires on its own, blocking every other execution in the meantime.

Need this built? Hire a Google Apps Script developer →

Sheet / project setup

ResourceNamePurpose
LockLockService.getScriptLock()Serialize critical section
CounterSHARED_COUNTER propertyIncremented safely
SheetCounter LogAppend each new value

What it does

incrementSharedCounter acquires a script-wide lock, reads the current counter value from Script Properties, adds one, writes the new value back to both Properties and a Counter sheet, and releases the lock.

Prerequisites

tryLock(30000) attempts to acquire the lock and waits up to thirty seconds for any other execution already holding it to finish, returning false instead of throwing if the timeout elapses first.

Walkthrough

No special authorization beyond the script's normal Sheets and Properties access is required; LockService itself does not need any scope the rest of the script does not already use.

Edge cases

The read-modify-write sequence, reading SHARED_COUNTER, computing next, and calling setProperty, all happens between the successful tryLock call and the finally block, which is exactly the section that needs to run without another execution interleaving.

Testing

If tryLock returns false because the lock could not be acquired within thirty seconds, the function throws a clear message rather than proceeding to increment the counter without protection, which would defeat the purpose of the lock entirely.

Hardening

Simulate concurrency by opening two separate execution contexts, such as running the function from two different browser tabs at nearly the same time, and confirm the counter increases by exactly two rather than sometimes only one.

Variations

LockService's script lock only serializes executions within the same script project, so if the same counter is also modified from a different Apps Script project or an external system, this lock alone will not prevent a race condition with that other system.

Full code: incrementSharedCounter()

Call incrementSharedCounter from any trigger or menu item that needs a safe, shared counter; the lock ensures overlapping calls never lose an increment.

function incrementSharedCounter() {
  var lock = LockService.getScriptLock();
  var acquired = lock.tryLock(30000);
  if (!acquired) {
    throw new Error('Could not acquire lock after 30 seconds; another execution is running.');
  }

  try {
    var properties = PropertiesService.getScriptProperties();
    var current = Number(properties.getProperty('SHARED_COUNTER')) || 0;
    var next = current + 1;
    properties.setProperty('SHARED_COUNTER', String(next));

    var sheet = SpreadsheetApp.getActive().getSheetByName('Counter') || SpreadsheetApp.getActive().insertSheet('Counter');
    sheet.getRange('A1').setValue(next);
    sheet.getRange('B1').setValue(new Date());

    return next;
  } catch (err) {
    console.error('incrementSharedCounter failed: ' + err.message);
    throw err;
  } finally {
    lock.releaseLock();
  }
}
  1. Line 2: Requests the script-wide lock shared by every execution.
  2. Line 3: Waits up to thirty seconds for the lock before giving up.
  3. Line 9: Reads the current counter value from Script Properties.
  4. Line 11: Writes the incremented value back to Script Properties.
  5. Line 15: Mirrors the counter value into a visible sheet cell.
  6. Line 22: Releases the lock in a finally block so it is never left held.

Deploy this example

  1. 01

    Open Apps Script

    In the bound spreadsheet: Extensions → Apps Script. For standalone projects, create one at script.google.com and link your Sheet by ID.

  2. 02

    Paste and save

    Add a .gs file, paste the code below, rename constants at the top (sheet names, column letters, API property keys), then save.

  3. 03

    Authorize once

    Run the main function from the editor. Accept OAuth scopes when prompted — triggers cannot run until authorization succeeds once.

  4. 04

    Add the trigger

    Triggers → Add trigger → choose the handler function and event (time-driven, on edit, or on form submit). Delete test triggers before production.

Before you run: serialize a shared counter

  • 1tryLock timeout chosen to comfortably exceed the critical section's expected duration
  • 2Lock release placed inside a finally block, not just at the end of the try
  • 3Failed lock acquisition treated as an error rather than ignored
  • 4Counter sheet and Script Properties key names confirmed to match
  • 5Concurrency tested with two near-simultaneous executions
  • 6Any external systems touching the same counter identified as an unprotected gap
  • 7Lock scope confirmed to be script-wide, not per-user

Frequently asked questions

tryLock returns a boolean indicating success or failure after waiting up to the given timeout, while waitLock throws a LockTimeoutException if it cannot acquire the lock in time, so the choice mostly comes down to whether you prefer to check a return value or catch an exception.

Script Properties avoids a round trip to the Sheets API for a value that is purely internal bookkeeping, though this tutorial also mirrors the value into a Counter sheet cell so it is visible to anyone looking at the spreadsheet.

They give up early and the function throws instead of incrementing the counter, so a timeout set shorter than realistic contention will cause otherwise-valid executions to fail unnecessarily.

No, a script lock is scoped to the single script project that created it, so a race between two separate projects touching the same data requires a different mechanism, such as a lock cell with optimistic checks.

A finally block guarantees the lock is released even if an error is thrown partway through the critical section, whereas releasing it only after a successful write would leave the lock held indefinitely if that write ever failed.

Yes, LockService.getDocumentLock() scopes the lock to the current document instead of the whole script project, which is more appropriate when the shared resource is specific to one spreadsheet rather than shared across every use of the script.

Related examples

Want this wired into your real workflow?

I adapt these patterns to your Sheet structure, APIs, and triggers — deployed in your Google account. Fixed-scope quotes from $500 · free 30-min consult · quote within 24 hours.