How appendRow keeps concurrent writes safe
appendRow does one thing that setValues cannot: it locates the first empty row and writes to it as a single atomic operation. That matters the moment two executions can overlap — a Google Form trigger is the clearest case. Two submissions arrive within the same second, both fire onFormSubmit, and both call getLastRow(). They read the same value, compute the same target row, and one overwrites the other. appendRow sidesteps that by letting the Sheets back end resolve the race.
The cost is one API call per row. If you're writing inside an onFormSubmit trigger that fires for a single response, that's fine. The execution budget for a simple trigger is six minutes, and a single appendRow typically takes under a second. Where appendRow starts to hurt is inside a loop: calling it 500 times in a migration script will crawl, and you'll likely hit the 30-second script runtime ceiling before you're done.
One practical note: appendRow always appends after the last row that contains any data, including rows with formulas or invisible whitespace. If your sheet has a stray space in row 200, your new row lands at 201, not where you expected. I keep a dedicated responses sheet with no trailing noise for exactly this reason.