MEASURED.
NOT CLAIMED.
Every number here comes from a run on a named machine against generated data. The parts that broke are on the page too — in the same type size as the parts that did not.
WHAT THIS SHOWS.
HubSpot's standard limit is 15,000,000 records per object. We have now run a sheet of 16,000,000 contacts on a small two-core machine, and on a developer laptop, with every step completing. Reaching any single record — the first, the last, the middle of a sorted view — stays between 7 and 42 ms, and the browser holds 11 to 17 MB while doing it.
That is the headline, and it has a shape. Anything that touches one record stays in milliseconds. Anything that computes over all sixteen million takes real time: a full sort is 75 seconds. Both numbers are on this page, because a page that only shows the first kind is a brochure.
Two things broke on the way, and we published both while they were broken. Both are fixed now, and the paragraphs that said so are rewritten rather than deleted — you can see what they used to claim. Telling a cost apart from a defect is most of what a benchmark is for; the list is below.
TWO MACHINES.
Both are single-user development setups, not production servers. Machine A is the interesting one: a small container, the kind of thing a modest self-hosted install runs on. The small machine is the stronger story, so it is the one most figures here come from.
| A — build container | B — developer laptop | |
|---|---|---|
| CPU | 2 virtual cores | 8 cores |
| Memory | 7 GB | 39 GB |
| Setup | Linux, database on the same host | Windows laptop, database in a container, over the network |
| Data | 16,000,000 generated contacts, 15 columns | the same |
Machine B has more hardware and a longer path to its database, and is not uniformly faster for it — on the heavier work it needs 1.0 to 1.8 times as long as the small container. Where the two differ by more than measurement noise, both are quoted.
The data is generated: contact records with the columns a real one carries, text rather than numbers, including the compliance fields. No customer record and no personal data was used or measured.
AT THE EXCEL ROW LIMIT.
Before the large sizes, the ordinary case: a twelve-column sheet held entirely in the browser. Edit p95 is the 95th percentile of 200 single-cell edits, each including a screen re-read.
| Rows | Machine | Load | First screen | Sort (text) | Filter | Edit p95 | Memory |
|---|---|---|---|---|---|---|---|
| 100,000 | A | 0.7 s | 47 ms | 0.35 s | 0.03 s | 0.1 ms | 83 MB |
| 500,000 | A | 4.6 s | 43 ms | 1.9 s | 0.07 s | 0.1 ms | 382 MB |
| 1,048,575 | A | 9.0 s | 53 ms | 4.1 s | 0.14 s | 0.1 ms | 793 MB |
| 1,048,575 | B | 9.7 s | 32 ms | 4.4 s | 0.15 s | 0.1 ms | 797 MB |
1,048,575 rows is a spreadsheet's ceiling exactly, and the point where most tools hand you a pagination control instead. Editing a cell does not depend on size at all; the first screen is readable in under 60 ms at every size, so work starts before loading has finished.
Note the memory column: 793 MB to hold a million rows in the browser. Keep it in mind for the next section, where sixteen times the data uses a fiftieth of that.
A CASE THAT TIPS BOTH EDGES.
Test sizes are usually round numbers somebody liked. This one is not. It comes from a customer loyalty programme — the kind of account where the volume edge and the privacy edge bind at the same time, and where three different tools each gave up for a different reason.
A loyalty programme is a long list of people and a very long list of things those people did. The volume is not the membership; it is every interaction attached to it. And because the whole point is behaviour, every one of those members carries a consent, a purpose, a retention period and the standing possibility of asking you to delete all of it.
Hit either edge and the usual answer is to split the problem across tools. Hit both and the splits start contradicting each other: the system that can hold the volume is not the one holding the consent, and the export that bridges them is the thing nobody wants to describe to a regulator.
| Tool | What it ends at | Where that lands |
|---|---|---|
| Airtable | Records per base | 50,000 on Team, 125,000 on Business, 500,000 on Enterprise Scale |
| A spreadsheet | Rows per sheet | 1,048,576 — and it is a list, not a set of related records |
| HubSpot | Not records (15,000,000) — relationships and fields | 50,000 associations per record, 1,000 custom properties per object |
Three ceilings, three different kinds. The first stops you by count. The second holds the count but flattens the relationships. The third carries the count comfortably and tightens as soon as each member has thousands of interactions behind them, or your programme needs more than a thousand fields to describe a tier.
That account holds ten million records. When we first published this page we had measured four. We have now measured sixteen — past the account, and past the largest of the three ceilings.
The shape of this test is real. The data in it is not. We took the form of that account — the volume, the ratio of interactions to members, the compliance fields a loyalty programme has to carry — and generated everything we measured on. No customer record and no personal data was used. That is also why this page shows a number and not a logo.
SIXTEEN MILLION CONTACTS.
Every step of every run below completed without error, on both machines. HubSpot's standard ceiling is 15,000,000 records per object, with higher tiers available at extra cost; this is 16,000,000.
| 4 M · A | 10 M · A | 16 M · A | 16 M · B | |
|---|---|---|---|---|
| First rows / jump to the last record | 9 / 7 ms | 16 / 14 ms | 10 / 7 ms | 12 / 10 ms |
| Jump to the middle / end of the sorted view | 6 / 10 ms | 15 / 9 ms | 15 / 19 ms | 12 / 12 ms |
| Save one cell / check for others' changes | 10 / 2 ms | 16 / 3 ms | 16 / 2 ms | 14 / 5 ms |
| Sort all records by last name | 16.2 s | 44.2 s | 75.0 s | 80.8 s |
| Filter by country, records shown | 4.3 s · 400,000 | 10.9 s · 1,000,000 | 18.8 s · 1,600,000 | 19.3 s · 1,600,000 |
| Count per country, all records | 6.8 s | 16.9 s | 27.3 s | 40.5 s |
| Build a list of 50,000 recipients | 3.8 s | 4.9 s | 5.2 s | 9.3 s |
The shape is the point. Reaching one record costs the same at sixteen million as at four — a jump across the whole sheet is a scroll of one screen. Work that computes over every record grows roughly in proportion to the data, and we publish that rather than rounding it away.
Worth saying plainly, because the numbers invite the wrong reading: a sort of sixteen million taking 75 seconds is not a fault. It is sixteen million comparisons on two virtual cores. Nobody gives you that for free, and nobody else publishes what it costs them.
For scale: holding all of this took about 7 GB of database storage, and seeding it took just under three minutes on machine A.
THE BROWSER DOES NOT GROW WITH YOUR DATA.
Not the server talking to itself — a real browser driving the sheet, three runs on machine A, median shown. The same script has since been run on the laptop, and it lands in the same range. The memory figure is the one worth keeping.
| Step | Median of three runs |
|---|---|
| Open the sheet until the first record is on screen | 1,256 ms |
| End key, to record 16,000,000 | 312 ms |
| 200 page-ups, then jump to record 8,000,000 | 1,622 ms |
| End on the sorted view | 238 ms |
| JavaScript memory after opening | 12.0 MB |
| ... after the paging and the jump | 15.9 MB |
| ... with the sorted view loaded | 16.4 MB |
Sixteen million records, and the tab holds about sixteen megabytes. Compare it with the 793 MB a single million costs when the browser carries the whole sheet. Your laptop stops being a function of how much data your company has.
On the laptop the same run gave 11.4 to 15.8 MB, the sheet open in 1.4 s and the End key at 365 ms — the same picture on different hardware, which is the point of running it twice.
The sort is the exception, and it is the server's 75 seconds rather than the browser's; the browser adds a tenth of a second to it.
NO LIST WITHOUT A CHECK.
A recipient list does not come into being by hand. It exists only through the sending step, and that step tests every single record against your own rules — by the same standard whether the list has a thousand records or sixteen million.
| 1 M · A | 4 M · A | 16 M · A | 16 M · B | |
|---|---|---|---|---|
| Check every record | 2.1 s | 7.4 s | 38.1 s | 55.7 s |
| Above the cap: refused and logged | 2.0 s | 7.5 s | 40.8 s | 64.1 s |
| Check a filtered view of 1,600,000 contacts | 1.2 s | 4.4 s | 10.5 s | - |
A list may hold up to 100,000 recipients. A larger one is refused, and the refusal is written to the record as an event — a refusal you can show someone next year is worth more than one that merely happened.
The last row used to be the one that did not look right. When we first published this page, checking a filtered view took 84 seconds — nineteen times slower for four times the data, and three times faster on different hardware. We wrote that it was a defect rather than a cost, said it was not fixed, and left it on the page. It is fixed now: the same check over 1,600,000 contacts takes 10.5 seconds.
The check reads your own rules. It is not legal advice, and it does not replace suppression lists, opt-outs or a channel-by-country policy. Today it produces a file with a log entry; channelSENDER sends over the approved lists1 as the first module on Podma.
¹ Planned for the next release.
400,000 ROWS, FULLY GUARDED.
A CRM is mostly relationships, and this is where tools quietly give up. Nothing here takes a shortcut: every record written is checked, attributed and logged on the way in.
| Machine A | Machine B | |
|---|---|---|
| 100,000 prospects + 100,000 projects + 200,000 relations, fully validated | 32.8 s | — |
| Open the prospects sheet / first screen | 1.2 s / 80 ms | 2.3 s / 191 ms |
| Autosave of one cell | 6 ms | 16 ms |
| Check for other people's changes | 1 ms | 4 ms |
| Remove one record and its relations from the sheets (148 s before) | 16 ms | 26–107 ms |
Record identity is assigned by the system and never reused, every field is validated, every relationship is checked against a record that actually exists, and each save leaves an entry you can read a year later. Loading does not slow down as the sheets fill — the last hundred thousand records arrive as fast as the first.
The last row describes removing a record from the sheets, and it is quoted as exactly that. Whether a person can be made to disappear from the system entirely is a separate question — see what comes next.
AHEAD — AND SAYING WHERE NOT.
HubSpot's figures are account limits from their own documentation, retrieved 30 September 2026. Ours are what was measured. The two answer different questions and are presented as such.
| HubSpot | Podma | |
|---|---|---|
| Records per object | 15,000,000 standard; higher tiers cost extra | 16,000,000 measured on a small 2-vCPU machine |
| Relations per record | 50,000 | no ceiling |
| Properties / columns per object | 1,000 custom | 16,384 |
| Custom objects | 20 (Enterprise) | registry-defined, no fixed number |
| The record after the close | ends at Closed Won | a project, with contract, DPA and retention |
The first row is the one we spent a month not claiming. It is a measurement now, on both machines, and it carries its machine because a number without one is marketing. Read it precisely: more than HubSpot's standard limit per object, not more than HubSpot. They sell higher tiers, and a sentence that leaves that out is one a competitor corrects for you in front of a customer.
What it does not mean is that every part of the product is comfortable at that size — the next section is about that.
Sources: HubSpot developer documentation, limits tracking API, the HubSpot Product & Services Catalog, and Airtable plans overview.
WHAT BROKE, AND WHAT THIS IS NOT.
- The start page broke here, and it is fixed. At sixteen million records it read too much at once and took the server down with it. It now opens that account in 8.4 seconds on two virtual cores — which is what counting sixteen million records off disk costs. That is the price of counting on arrival; not counting on arrival is the next step. We publish the number because it is the slowest thing on this page, not because we think it is a bad one.
- The other defect is fixed too. Checking a filtered view for who may be contacted took 84 seconds when this page first went up; it takes 10.5 over 1,600,000 contacts now. Neither of those sentences was removed quietly — both are rewritten above, still carrying what they used to say.
- Not more records per object than HubSpot can sell you. 16,000,000 is past their standard limit of 15,000,000, not past the higher tiers they offer at extra cost. The honest claim is "more than the standard limit", with the machine.
- Not production, not multi-user. Every figure here is single-user, on a development machine. Concurrent load is unmeasured.
- Not a measured time in another meaning. The 16 ms above removes a record from the sheets. The change history still holds what the values were, so that number is not a claim about erasing a person — see what is announced below.
- Not database-enforced tenant separation. Every request and every write is scoped to its tenant in the application today, and every logged change names the tenant. Enforcement in the database itself is announced below, not claimed here.
- Not real data. Everything is generated. No customer record and no personal data was used or measured.
- Not rounded. 16,000,000 is 16,000,000. Where a figure varies between machines, both are given rather than an average.
If a number here matters to a decision you are making, run it yourself. Ask, and you get the raw output or a session on your own export.
WHAT COMES NEXT.
Four of the limits above are closing. The concepts are settled and the releases are planned, so they are written here as what CMP42 does — with the footnote until the day they run.
- Erasure that reaches the audit trail.1 Personal values in the change history are held encrypted per person; a deletion destroys that person's key. The log stays verifiable, the values stop being readable.
- Retention and legal hold, enforced.1 A removal is refused while a legal hold is set or a retention date is still in the future.
- Tenant separation enforced by the database.1 Not only by the application that queries it — so a mistake in the application cannot cross the line either.
- Access control per Model, with fields masked per role.1 A role can read a contact with the phone number hidden.
¹ Planned for the next release.
None of these has a number on this page, because a plan does not produce measurements. When each one ships, the footnote comes off the claim and the sentence describing its absence comes off the page in the same change.
DON'T TAKE OUR WORD FOR IT.
A benchmark you cannot interrogate is a brochure. Ask, and you get the rest of it under NDA — because what sits behind these numbers is the product.
- The raw output of every run quoted on this page, with its timestamp and the machine it came from.
- A session on your own data. Bring an export of your largest object and we run the same measurements against it while you watch.
- The measurements we have not published — including the ones that did not flatter us.
- A straight answer on where it breaks. Two of those answers are already on this page, both fixed within a day of being published — and still readable in the form they had while they were broken. There are more.
- And an open invitation. We cannot measure HubSpot at this size: that would need an account with sixteen million records, and whoever has one is unlikely to be allowed to publish what it does. If you bring a large one, we will run the comparison with you and publish whatever comes out — including the parts that do not favour us.
Design Partners get this in the first conversation rather than as a favour. Twelve places, six months, and your rate stays yours →