n8n workflows
n8n scraping to Google Sheets: one status row per URL, no 429s
Write one status row per URL from n8n to Google Sheets without hitting the 60-writes-per-minute quota: where the Sheets node goes, which columns, and what to leave out.
By Caitlin Sanders, AgentScraper team · Published
This article was drafted with AI assistance from the sources listed below and edited and published by the AgentScraper team. Disclosure: the AgentScraper API, which this article mentions, is this site's own product.
- 1Read URLsOne item per URL from the sheet.
- 2HTTP Request with BatchingFetch every URL in one node run.
- 3Edit Fields (Set)Keep url, status, error_tag, fetched_at. Drop the page body.
- 4Append or Update Row, onceOne Sheets node run per execution, matched on url.
- 5Error output to WaitOn a Sheets 429, wait one minute and write again.
Your n8n workflow fetches 300 URLs and writes each result to a Google Sheet. Partway through, the Google Sheets node fails with "too many requests" and the run stops. Or it finishes, and the sheet holds 300 cells of Markdown that nobody can scroll through. Both come from the same habit: treating the sheet as storage, and writing to it inside the loop.
The fix is a sheet that holds one status row per URL, written once per execution.
What the Sheets quota counts
Google's Sheets API allows 60 write requests per minute per user per project, and 300 per minute per project. Reads have the same two limits. The quota refills every minute. Above it, the API answers 429: Too many requests.
"Per user per project" means per Google credential. Two workflows that use the same credential share the same 60 writes.
The number that matters is how often the Sheets node runs, not how many rows it writes. The n8n source for Append or Update Row shows what one run does with all the items it receives:
- It reads the sheet data and the values of the match column.
- It sends one batch update for every row that matched.
- It sends one append for every row that did not.
A batch request counts as one request toward the quota, including everything inside it. So 300 rows written in one node run cost a few requests. The same 300 rows written in a loop, one pass per second, cost a few requests per pass, and you reach 60 writes in well under a minute.
Build the write-once layout
The workflow fetches all URLs in one node run, shapes the results, then writes once. Pacing comes from the HTTP Request node's Batching option, as in Batching or Loop Over Items.
Step 1. Add a Google Sheets node with Get Row(s). Rename it "Read URLs".
What you see: one item per row, each with a url field.
Step 2. Add an HTTP Request node. Open Options → Batching. Set Items per Batch to your plan's rate limit in URLs per second and Batch Interval (ms) to 1000. The body sends one URL per item. The node settings for AgentScraper API are in the no-code recipes.
{ "urls": ["{{ $json.url }}"] }
What you see: one output item per URL, each with results and statuses.
Step 3. Add an Edit Fields (Set) node with four fields:
| Field | Value |
|---|---|
url | {{ $('Read URLs').item.json.url }} |
status | {{ $json.statuses[0].status }} |
error_tag | {{ $json.statuses[0].error ? $json.statuses[0].error.tag : '' }} |
fetched_at | {{ $json.statuses[0].fetchedAt }} |
What you see: small items, no Markdown.
A failed page comes back inside the normal response with a status that is not success, so it gets a row like every other URL. Failed fetches cost 0 credits, and the row is how you find them.
Step 4. Add a second Google Sheets node with Append or Update Row. Set Column to Match On to url. Set Mapping Column Mode to "Map Each Column Manually" and map the four fields.
What you see: one row per URL, updated in place on the next run.
If you use "Map Automatically" instead, check Handling extra fields in input. Its default is "Insert in New Column(s)", so a stray markdown field becomes a new column full of page bodies. The node source also writes header changes for new columns item by item, which costs extra requests.
Step 5. In the second Sheets node, open Settings and set On Error to "Continue (using error output)". Connect the error output to a Wait node set to 1 minute, then to a copy of the Sheets node.
What you see: a rare 429 costs one minute instead of the run.
Google's own advice for a one-minute quota is to wait a minute or retry with exponential backoff plus random jitter, up to a maximum backoff of 32 or 64 seconds, with a cap on the total retries.
If you need Loop Over Items
Some workflows run an AI step per batch and need Loop Over Items. Keep the fetch and the model inside the loop, and place the Sheets node after the loop has finished, not inside it.
For long lists, split the work into sub-workflow executions, as the n8n web scraping guide describes. Each child execution then writes its own chunk once. Your write count per minute is now the number of child executions per minute, times a few requests each. Count it before you schedule the job.
The Execute Once trap
A common forum fix for Sheets 429s is to turn on Execute Once in the node's settings. n8n's docs describe it plainly: the node runs once, with the first item it receives, and ignores the rest. On a read node that may be what you want. On a write node it silently drops every row but one.
Keep the page body out of the sheet
The sheet is the run log, not the archive. Google's help says a spreadsheet holds up to 20 million cells or 100MB, and that a cell over 50,000 characters is removed when an Excel file is converted. Long pages of Markdown are a poor fit for a cell. Send the Markdown to your AI step or a file. Write the model's short output (a label, a two-sentence summary) to the row, next to the status.
status in the sheet | What the next run does |
|---|---|
success, recent fetched_at | Skip the URL |
success, fetched_at past your refresh window | Fetch again |
error, with a temporary error_tag | Fetch again on the next run |
error, with a permanent error_tag | Leave it out and fix the list |
| empty | Never fetched: fetch it |
What this means for your workflow
- The quota counts Sheets node runs: 60 writes per minute per Google credential.
- Fetch the whole list in one HTTP Request run with Batching, then write once.
- Write
url,status,error_tagandfetched_at, not the page body. - Append or Update Row, matched on
url, with columns mapped by hand. - On a Sheets
429: error output, Wait 1 minute, write again. - Never use Execute Once on a node that writes rows.
Frequently asked questions
What is the Google Sheets API write quota that n8n runs into?
Google lists 60 write requests per minute per user per project and 300 per minute per project, with the same figures for reads. The quota refills every minute. Above it, the API answers 429 Too many requests.
Does the n8n Google Sheets node send one request per row?
Not for Append or Update Row. In the node source, one run reads the sheet, then sends one batch update for the rows that matched and one append for the new rows, for all items it received. The cost grows with the number of node runs, so a Sheets node inside a fast loop is what uses up the quota.
Should I turn on Execute Once to stop the 429 errors?
Not on a node that writes rows. Execute Once runs the node with the first item only and ignores the rest, so the sheet would get one row per run and lose every other URL.
Can I store the scraped Markdown in a Google Sheets cell?
It is a poor fit. Google's help says a converted spreadsheet holds up to 20 million cells or 100MB, and that cells over 50,000 characters are removed when an Excel file is converted. Keep the body in a file or pass it to your AI step, and write the status and the model's short result to the sheet.
Sources
- Usage limits (Google Sheets API) (Documentation)
- Google Sheets sheet operations (n8n docs) (Documentation)
- Google Sheets node, Append or Update Row operation (n8n source) (Documentation)
- Node settings (n8n docs) (Documentation)
- Files you can store in Google Drive (Google help) (Documentation)