Google Sheets as your form database: how far it goes
Rows, columns, queries your whole team can read, permissions, an API. That is a database - up to a point this guide is honest about.
The unfashionable truth: a sheet is a fine database for this
Developers reflexively apologize for storing data in a spreadsheet. For form submissions specifically, the apology is usually misplaced. Lead capture is an append-only stream of small, flat records that humans - not applications - need to read, filter, annotate, and act on. That workload is what spreadsheets are for. The marketing team can open it, sort it, add a "Called? " column, and build a pivot table without filing a ticket, which no Postgres instance offers.
SheetLink Forms treats the sheet as the working system, not an export target: submissions are screened, mapped, and appended as rows by a worker writing directly to the Google Sheets API, with retries and a delivery log. The question this guide answers is not whether that works - it does - but how far it goes, and how to recognize the far end honestly.
What makes the sheet-as-database work well
A few product mechanics do the database-shaped chores:
- Schema: your header row is the schema. On an empty sheet a header row is seeded for you; from then on, field names matching headers map automatically (case and punctuation insensitive -
Email,email,E-mailall match), with explicit per-field mapping for the outliers. - Write path: rows append under the header asynchronously, retrying at 5, 30, and 120 minutes if Google blips - your form never waits on the write.
- Input sanitization: a formula-injection guard escapes leading
=,+,-, and@, so a hostile submission cannot plant a live formula in a cell. - Junk control: spam screening with quarantine keeps the table clean - a database is only useful if its rows are mostly real.
- Access scope: the connection uses
drive.file, so SheetLink can touch only sheets you picked or created in the product - never the rest of your Drive.
Downstream, the ordinary Sheets toolbox becomes your query layer: filters and filter views, pivot tables, QUERY(), charts, and connectors like Looker Studio. The setup walkthrough is at HTML form to Google Sheets without a backend.
The hard limits, with numbers
Now the honest part. Google Sheets has a documented ceiling of 10 million cells per spreadsheet - all tabs combined. A 20-column leads sheet hits that at 500,000 rows, which sounds distant until you notice the practical ceiling arrives far earlier: a sheet with tens of thousands of rows and living formulas (VLOOKUPs, QUERYs, conditional formatting across whole columns) gets sluggish to open and recalculate long before any hard cap. For most teams the felt limit is "the sheet got slow", somewhere in the tens of thousands of rows, not "Google refused the write".
Other edges worth knowing: a cell holds at most 50,000 characters (irrelevant for form fields under the 256 KB submission cap, but real if you concatenate); whole-column volatile formulas multiply recalculation cost with every appended row; and a spreadsheet is one file - there is no partitioning, only new tabs or new files.
Concurrent edits: what happens when humans and the worker share a sheet
Google Sheets handles concurrent editing well for its intended case - multiple humans typing. Your case adds a machine appending rows while humans work, and two behaviors deserve attention:
- Sorting the data range while rows append. Appends land under the existing rows. A colleague who sorts the whole range, inserts rows mid-table, or leaves stray content below the data does not break delivery, but the interleaving can be confusing - new rows keep arriving at the bottom while the view above them was just rearranged.
- Edits racing appends. There are no transactions. A human annotating "their" row while new rows land is fine; a human restructuring columns mid-stream (renaming headers, inserting columns) changes the mapping contract under the worker's feet - matching follows headers, so a renamed header stops matching its field.
The convention that avoids all of it: treat the capture tab as append-only. Nobody sorts it, nobody edits it; annotation and analysis live on a second tab that references the first (or in personal filter views, which reorder your view without touching the shared order). Header changes are deliberate schema changes - rename the field or the mapping at the same time.
Signs you have outgrown the sheet
Clear signals, in rough order of arrival:
- You need joins. Leads referencing accounts referencing deals is relational work;
VLOOKUPchains across tabs are a database schema wearing a costume. - You need row-level permissions. Sheet sharing is all-or-nothing per file; "reps see only their leads" is not a sheet feature.
- An application reads it in the serving path. Rendering your website or app from live sheet reads puts API quotas and latency where users feel them.
- The sheet got slow - the practical ceiling above.
- Compliance asks for an audit trail. Revision history is not a queryable log of who changed what field when.
None of these mean the form pipeline was a mistake; they mean the sheet's second job (system of record) has outgrown its first (capture and triage).
Graduating without breaking what works
The migration is gentler than it sounds, because capture and storage are separable. The sheet keeps the job it is best at - the human-facing capture ledger where new leads land, get glanced at, and get acted on - and a periodic sync (a scheduled script, a warehouse connector, or an import job reading the sheet) moves rows into Postgres, BigQuery, or your CRM as the durable system of record. Your form, endpoint, and screening do not change at all.
If you would rather re-point the pipeline itself someday, that is a dashboard change, not a code change - the destination is a per-form setting, and Excel Online tables are the other built-in option (see sending submissions to Excel). For the full picture of what happens between POST and row, read how it works, or watch the demo feed a public sheet. Free during beta - invites at the waitlist.
FAQ
How many form submissions can a Google Sheet actually hold?
The hard cap is 10 million cells per spreadsheet across all tabs - 500,000 rows at 20 columns. The practical ceiling is earlier: with live formulas and formatting, sheets tend to feel slow in the tens of thousands of rows. Plan around the practical ceiling, not the cap.
What happens if someone sorts or edits the sheet while submissions arrive?
Delivery keeps working - rows append under the existing data. The risks are human confusion (new rows arriving at the bottom of a freshly sorted view) and broken mapping if someone renames a header mid-stream, since fields match headers. Keep the capture tab append-only and use filter views or a second tab for analysis.
Can a submission inject a formula into my sheet?
No - values beginning with =, +, -, or @ are escaped by the formula-injection guard before they reach a cell. Combined with spam quarantine, the sheet stays a table of data, not an execution surface.
Should I split submissions across multiple sheets or tabs?
Split by purpose, not by volume, at first: one form, one sheet or tab, so each ledger stays coherent. If a single form's volume approaches the practical ceiling, archive older rows to another file periodically - the capture sheet stays fast and the history stays queryable.
When should I move to a real database, and what happens to my form?
Move when you need joins, row-level permissions, application reads in the serving path, or an audit trail. Your form does not change: keep the sheet as the capture layer and sync rows out on a schedule, or re-point the form's destination in the dashboard. The endpoint and screening are independent of where rows ultimately live.
Related guides: Send an HTML form to Google Sheets without a backend · Send form submissions to Excel Online automatically · Form endpoint vs writing your own serverless function