Built for a staff list, not an audience
Somewhere in every architecture conversation, someone asks it. You have described the design — the logic lives in the database, the browser draws what it is told — and there is a pause, and then: but won't that hammer the database?
It is a fair reflex. It is also, nearly always, the wrong question, because it has been borrowed from a different kind of software.
Most of what has been written about scale in the last fifteen years was written by, and for, people building consumer platforms. A social network, a search engine, a video site: hundreds of millions of anonymous users, overwhelmingly reading. In that world the database really is the scarce resource, and the whole craft becomes keeping traffic away from it — caches in front, read replicas beside, queues behind, eventual consistency accepted as the price of survival. If one reader in a thousand sees a post a second late, nobody files a ticket.
Business software is not that. Its users are on the payroll, and its failure mode is a wrong number, not a slow feed.
Count them
An ERP, a WMS, a CRM serves the people an organisation employs. Dozens, hundreds, in a large company a few thousand. That is not a market, it is an org chart — bounded by hiring, known by name, each one authenticated before they touch anything.
And they are not browsing. They are entering orders, booking receipts, moving stock, correcting a line someone got wrong this morning. The traffic is write-heavy, authorised, and consequential in a way a timeline never is.
So the two kinds of system have opposite scarce resources. On a consumer platform the user count is unbounded and the consistency requirement is soft. In a warehouse the user count is bounded by the number of door badges and the consistency requirement is absolute: a stock mutation is recorded or it is not, and "eventually" is not one of the options, because someone is standing at the rack holding the pallet.
Optimise for the wrong one and you get a beautifully scalable system that occasionally writes half an order.
What actually loads a database
The worry is about load, so let us be precise about where load comes from in a business application. It is rarely the work. It is nearly always the chatter.
The familiar shape: a request arrives, a controller asks the ORM for an order, the ORM issues a query. Iterate the lines — another query per line. Each line wants a product and a price, two more each. The rows become entities, the entities become DTOs, the DTOs become JSON. Forty short queries and three copies of the same data in memory, to draw one screen. Not one of the forty is slow. Together they are the load, and most of that load is round trips and object mapping rather than anything the database found difficult.
TSQL.APP reaches the same screen differently. The request is flattened into a table of rows and passed as a single parameter, on one connection, to one stored procedure. That procedure does the work as set-based T-SQL, next to the data, and streams named result sets back. The API labels them and hands over the JSON. The browser turns the JSON into a screen.
One round trip. No ORM, no DTO layer, no N+1. The T-SQL never builds HTML — what goes over the wire is a compact description of what should be on screen, and the rendering, the fiddly per-user part, happens on the workstation of the person looking at it. Which is the one machine in the whole system that is reliably idle.
That is the entire answer to "won't it hammer the database?" It does less work than the architecture the question assumes, not more.
Here is a write path, whole, from the order form on the homepage:
sqlIF @SubmitButton IS NOT NULL
BEGIN
IF @CustomerID IS NULL OR @ProductID IS NULL OR ISNULL(@Quantity, N'') = N''
BEGIN
EXEC sp_api_toast @text = N'Please fill in all required fields',
@class = N'btn-warning';
RETURN;
END
INSERT INTO orders (customer_id, product_id, quantity)
VALUES (@CustomerID, @ProductID, @Quantity);
EXEC sp_api_toast @text = N'Order created successfully', @class = N'btn-success';
EXEC sp_api_modal_clear;
END
No transaction is opened around that INSERT. That is not an oversight.
The part worth knowing
The kernel opens one named transaction early in the request, and everything after it —
identity, metadata, the card, and the whole action script — runs inside it. The request
boundary is the transaction boundary. A script does not manage a transaction because
it is already in one, and it must not open its own: a stray BEGIN TRAN without a
matching COMMIT loses the request's writes when the pooled connection resets, and a
bare ROLLBACK kills the kernel's transaction outright.
That boundary is what closes the gap the three-tier stack leaves open. The usual half-saved-order story is not really a database failure — it is two copies of the truth drifting apart, one in the tables and one in a client-side state manager that has already moved on to the confirmation screen. Here the browser holds no application state to drift. It holds the answers the user has typed, and hands them back to a script that decides everything again from scratch, inside the transaction, every time. The screen and the writes come out of the same execution or neither does.
Instead a script signals, and the signals are raised as errors — severity used as a control channel rather than an error level:
sqlRAISERROR('success', 13, 1); -- stop, commit, render what is on screen
RAISERROR('no-success', 13, 1); -- stop, roll back, still render the modal
RAISERROR('Cannot delete|This invoice is already booked.', 11, 1);
Severity 11 is the sanctioned way to put a titled error dialog in front of a user: the text splits on the pipe into heading and body, and it is treated as a normal outcome rather than a crash.
One neat corner of this: toasts survive a rollback. They are copied into a table variable, and table variables are not transactional — so "could not book this invoice" is still on screen after the writes that failed have been undone.
And the honest limit, because it is the thing people get wrong: the transaction is per request, not per wizard. A five-step dialog is five transactions. An error on step four does not undo what step two committed. Do the writing after the last question — which is what the replay model wants from you anyway.
The question to ask instead
Not "how many users could this take", but "how many users does this actually have, and what happens to one of them when something goes wrong". For a public platform the first question is the whole game. For the software a company runs itself on, the second one is the only one that has ever mattered.
There is documentation and a programming reference online, or you can book a demo and bring the sceptical question with you.