Robert O'Neill ·

We tested Google Sheets and Excel Online. Their APIs silently overwrote our data.

More and more software writes to spreadsheets: automations, sync jobs, and lately AI agents working on the same files as the people who own them. Almost all of it follows one pattern: read the sheet, compute something, write the result back.

We wanted to know what happens when the sheet changes between the read and the write. So we ran the same test against three spreadsheet APIs.

The test

Two writers read the same cell. Writer A writes a new value. Then writer B, which never saw A’s change, writes its own value to the same cell.

Writer AWriter B (stale)What’s left in the cell
Google Sheets API (values.update)200 OK200 OKB’s value. A’s edit is gone, with no error.
Excel Online, Microsoft Graph (range PATCH)200 OK200 OKB’s value. A’s edit is gone, with no error.
VisiGrid API (write_cells)200 OK409 revision_conflictA’s value. B is told to reread.

This isn’t a race we had to get lucky to hit. It reproduces every time, because neither API can express the condition. Sheets’ values.update has no revision or precondition parameter (reference). Graph’s range update takes no If-Match (reference). Graph supports ETags on whole-file uploads, but not on cell edits.

To be clear about scope: this is about the APIs. People editing together in the browser are fine in all three products, which merge edits live. The problem is the program that read a minute ago and writes now.

Reproduce it

The full scripts are at github.com/VisiGrid/stale-write-test. They use only curl and jq. The Google Sheets half is just this:

url="https://sheets.googleapis.com/v4/spreadsheets/$SHEET_ID/values/A1?valueInputOption=RAW"
put() { curl -s -o /dev/null -w '%{http_code}\n' -X PUT "$url" -H "Authorization: Bearer $GOOGLE_TOKEN" \
          -H 'Content-Type: application/json' -d "{\"values\":[[\"$1\"]]}"; }

put "original"   # both writers read "original"
put "writer A"   # 200
put "writer B"   # 200, computed from "original"; A's edit is now gone

The VisiGrid half differs in one field. Every write names the revision it was computed from:

curl -X POST "https://app.visigrid.app/api/connector/sheets/$SHEET_ID/cells" \
  -H "Authorization: Bearer $VISIGRID_TOKEN" -H 'Content-Type: application/json' \
  -d '{"expected_revision": 7, "edits": [{"ref": "A1", "value": "writer B"}]}'
# 409 revision_conflict: A already moved the workbook to revision 8

“Why are you using a spreadsheet for this?”

Fair question; we’d ask it too. The honest answer is that nobody chose the spreadsheet for this. The budget, the inventory list and the pipeline tracker already live in one, because the people who own the data can read and edit it without an engineer. Now software is writing to those same files the way it writes to a database, without the one guarantee every database has had for decades: compare-and-swap. That means a write that only applies if nothing changed since you read.

That’s what we built. Every VisiGrid write carries the revision it was computed from. If anything changed since (a person in the browser, another script, an agent), the write is refused with a 409, and the caller rereads and retries. Writes are atomic (up to 1,000 cells in one call), formulas are computed by the real engine before the response returns, and the response tells you if your write replaced a formula with a value.

”Isn’t last-write-wins just documented behaviour?”

Yes, and we’re not calling it a bug. The point is that the APIs give you no way to ask for anything else on a cell write, and they don’t tell you when it happened. If you’re wiring an agent or a sync job into a shared sheet, you’re one stale read away from erasing someone’s work without either of you knowing.

The other wall: write quotas

Every Sheets integrator eventually hits Google’s write quota: 60 write requests a minute per user. Here are the published limits side by side:

Write limitCells per writeOver the limit
Google Sheets API60 a minute, per userone range429 “Quota exceeded … Write requests”
VisiGrid API300 a minute, per tokenup to 1,000, atomic429 with Retry-After

We measured both. One client wrote 100-cell blocks as fast as each API answered, for two minutes. (Google Sheets and VisiGrid only; see “Method”.)

Writes in 2 minutesRefused
Google Sheets API170 (about 85 a minute)96, starting after 25 writes
VisiGrid API600 (exactly 300 a minute)42, starting after 300 writes, each saying when to retry

Both APIs have a wall; ours is five times further out and tells you when it lifts. It’s a policy, not a ceiling: before the limit existed, the same test sustained 633 writes a minute from one client.

And per-call latency (p50 / p95, ms, from the same machine, 15 runs each):

Google SheetsVisiGrid
read 1 cell110 / 171105 / 109
write 1 cell115 / 144109 / 112
write 100 cells120 / 131116 / 137
write 1,000 cells121 / 254177 / 461
4,000-row sheet: write a cell, read the recalculated total221 / 263433 / 493

Small calls are about even, with VisiGrid steadier at the tail. We’re not faster at everything: Google wins on large writes and big recalculations, by about 2× on the 4,000-row sheet. Our engine recomputes the whole sheet on each write, and incremental recalculation is next.

Method and caveats

  • When: 2026-10-08.
  • Where: all three from one client machine in the US, against each service’s production API. VisiGrid’s production is one server with 1.5 CPU cores; Google and Microsoft run on global infrastructure.
  • Google: a service-account client writing to a spreadsheet shared with it. Default quotas, nothing raised.
  • Excel Online: Microsoft Graph, delegated access, with one persistent workbook session. Microsoft’s API terms don’t allow performance testing of its services without a written agreement, so for Excel we report only the stale-write behaviour, and no timings.
  • VisiGrid: a personal API token (read and write) on the same connector API that Claude and other MCP clients use.
  • Sustained test: one client, one request at a time. Google’s write quota is per user, so a single client is the relevant case. A 429 is counted as a refusal and the client waits a second before its next write.
  • The stale-write test is deterministic. It doesn’t depend on timing, load or scale.
  • Harness: the latency, quota and recalculation numbers come from bench.mjs in the same repository. Run it from your machine; your numbers will differ with location.

Common questions

Does this affect people editing in the browser?
No. Google Sheets, Excel Online and VisiGrid all merge live edits between people in the browser. This is about programs that write through the API from data they read earlier.
Can I prevent it in the Google Sheets API?
Not on a cell write. values.update has no revision or precondition parameter. The usual workarounds are a lock you build yourself, or reading again right before every write, which narrows the window but doesn't close it.
Can I prevent it in Microsoft Graph?
Not on a range update. It takes no If-Match header. Graph's ETags apply to whole-file uploads, not cell edits.
What are VisiGrid's API limits?
300 writes and 600 reads a minute per token, and each write can carry up to 1,000 cells. Over the limit, the API returns 429 with a Retry-After header.
What does VisiGrid do differently?
Every write names the revision it was computed from. If the workbook changed since then, the write is refused with 409 revision_conflict and nothing is written, so the caller rereads and decides again.