One instance per customer – scaling .NET, SQL Server and S3 worldwide

One Windows VM running IIS, one MSSQL server and S3 for the documents – once per customer. That model carries you surprisingly far. It also does not break under load: it breaks at three predictable points – licence cost, maintenance windows and distance. This article maps the route in four stages – up to regional cells, meaning complete, mutually independent copies of the whole solution, one per world region. And it says plainly which stage most vendors actually need.

The short version

  • Load is rarely the bottleneck. It is licence cost per customer, maintenance windows multiplied by N and travel time across continents.
  • The governing rule: a tenant's application, database and documents belong in the same data centre. Everything else follows from that.
  • The number that explains it: 100 sequential queries cost roughly 0.1 seconds inside your own data centre – and roughly 26 between Frankfurt and Sydney.
  • What a "cell" is: a complete, self-contained copy of the solution – its own web nodes, its own database, its own document storage, its own backups. It carries a hard-capped number of tenants and never calls into another cell. When it is full you build the next one rather than making it bigger. AWS calls this a cell; Microsoft calls the same pattern deployment stamps.
  • Four stages – stage 0 is where you are today, then separate and pool, the first complete cell and several regions with a control plane. Most vendors need stage 2, not stage 3.
  • Two deadlines are pressing: .NET 8 and .NET 9 both fall out of support on 10 November 2026; the Windows Server 2022 (ltsc2022) container base images leave mainstream support on 13 October 2026.
  • The most expensive surprise hides in the SQL Server licence, not in the architecture.

The starting point

The setup is more common than its reputation suggests. A software vendor runs a web-based line-of-business application on .NET. Every customer gets their own instance – a Windows VM with IIS, a Microsoft SQL Server and S3-compatible object storage for the documents. Each customer has their own stack, their own database, their own address.

This is not a backward model; it is a deliberate choice with real advantages. In SaaS vocabulary it is called silo isolation: complete separation per tenant. Data cannot bleed across, one customer cannot starve another, restore and deletion always concern exactly one party and where a customer needs something bespoke, they can have it. That is precisely why siloed installations sell so well into regulated industries.

The price is equally clear – and it only becomes visible when ten customers turn into a hundred and one country turns into a dozen.

Where it breaks first

The order in which this model tears in practice is remarkably stable. It has little to do with compute load:

More hangs on that ordering than it first appears: the first three items bite before geography matters at all. Many vendors therefore believe their problem is "we need to go global", when what it actually is reads: "we can no longer serve the customers we already have properly." That is good news, because the fix for it is considerably cheaper than a second region.

Worth being clear about: none of these points demands giving up the silo model. They demand industrialising it. The difference between a vendor with 20 instances and one with 2,000 is rarely the architecture of the application – it is whether an instance is a product off a production line or a one-off from the workshop.

The governing rule: things that belong together stay together

Before any question of detail there is a single rule and almost everything else follows from it:

A tenant's application, database and document storage always live in the same data centre.

The reason is arithmetic. Light travels through fibre at roughly 204,000 km/s – a little over 68 per cent of its speed in vacuum. That is just under 9.8 milliseconds of round-trip time per 1,000 kilometres of fibre and fibre does not follow great circles: measured latency typically runs 1.5 to 2 times the geodesic distance. Measured from Frankfurt that is roughly 94 milliseconds to the US east coast, 166 to Singapore, 197 to São Paulo and 266 to Sydney – and that is across a well-built provider backbone, so it is the favourable case. Within Europe the figures sit between 9 and 16 milliseconds. None of this is negotiable – no provider, no protocol and no budget beats physics.

What matters is what an application does with them. Line-of-business applications are rarely frugal with round trips and the slowdown is exactly linear in round-trip time. A hundred sequential queries cost about a tenth of a second inside your own data centre – the same hundred queries between Frankfurt and Sydney cost around 26 seconds. And 100 is not a malicious assumption: 400 queries behind a single list view is a documented real-world figure and on the same route that lands at close to two minutes. The reason this hits so hard: on a developer machine the round-trip time is roughly 0.05 milliseconds, across a continent it is 100 to 266. That is a factor of 2,000 to 5,000 applied to a constant that appears nowhere in the code. It is exactly the point at which customers report "the software is slow" and developers find nothing in the profiler.

On top of that comes everything that happens before the first byte of payload: setting up a connection costs three to five round trips. A cold TLS connection at Sydney distance therefore burns close to a second before any data flows. And even afterwards TCP holds you back: one megabyte takes about seven round trips out of the initial window – nearly two seconds at Sydney distance against 0.7 to the US east coast. Bandwidth does not help with any of this. Only round-trip time counts.

Request path: the browser reaches the anycast edge, which handles TLS and the WAF, and the request then goes to the stateless IIS nodes inside the cell, whose database query in the same data centre costs under a millisecond. The web node replies with a signed URL, after which the document transfers directly between the browser and the S3 storage.
Figure 1: short hops inside the cell – and documents that bypass the application server.

From the governing rule follows a second one: you only speak across the long distance once. The browser talks over the distance to the nearest point of presence; everything after that happens locally. What crosses the ocean is the finished answer – not a hundred individual SQL queries.

What the edge really buys – and what it does not

A point of presence close to the user is the single most effective lever against distance, but not for the reason most people assume. The benefit lies not in caching but in splitting the connection: the edge terminates TLS, absorbs the three to five expensive set-up round trips over the short hop and then forwards the request over a connection to the origin that is already open and warm. Five long round trips become five short ones plus one.

Three common expectations of the edge do not survive contact with practice:

And one detail that is regularly assumed wrongly during selection: not every service marketed as "global" actually runs on anycast. It is worth checking that for the specific tier you are buying rather than taking the product page at its word.

Four stages

The road from a single VM to a worldwide fleet breaks into four stages. Each one buys off a specific problem and costs specific complexity in return. They build on each other, but you do not have to walk to the end:

Four stages stacked vertically: stage zero is one VM per customer and breaks on licence cost, maintenance windows and distance. Stage one separates web, database and object storage and pools many tenants per instance. Stage two builds the first complete cell with several web nodes, a database replica and immutable backups. Stage three runs one cell per region, steered by a tenant catalogue.
Figure 2: every stage solves a different problem – most vendors need stage 2.

1.Separate and pool

The first step costs the least and returns the most: the three layers stop living together on one machine. "One VM per customer" becomes a web pool, one or a few SQL instances and an object store – with many tenants sharing those building blocks.

Database separation survives the move: one database per tenant, but many databases on one instance. In practice that is the best compromise between isolation and unit cost. A SQL Server instance carries up to 32,767 databases, so the number of tenants is never the limit. The limits are memory, connections and manageability and depending on data volume they sit somewhere between a few dozen and a few hundred tenants per instance.

What this stage buys off:

What it costs: from here on there are neighbours. A tenant with a runaway query can noticeably slow down the others. The countermeasures exist – Resource Governor, separate resource pools, hard query timeouts – but they have to be set from day one. From SQL Server 2025 the Resource Governor is available in Standard edition as well; before that it was Enterprise only.

2.The first complete cell

Now the pool becomes a resilient unit – the first complete cell: web nodes, database, object storage, backups and monitoring as one package that runs on its own. Being self-contained is not an end in itself. Because a cell shares neither database nor storage with any other and never calls into one, a fault inside it simply cannot reach the rest – which is what distinguishes it from a merely bigger server. And because every cell is built the same way, the second one is no longer a project but a repetition.

For this particular stack that means:

This stage buys off the maintenance window for good: a node is drained, updated and returned to service while the others keep working. It also buys back the nights someone used to spend supervising a reboot.

How big a cell should be is a trade-off with very simple arithmetic: the blast radius of an outage is the reciprocal of the cell count. Ten cells mean a total failure hits ten per cent of customers; a hundred cells, one per cent. Smaller cells contain the damage and raise the management overhead – larger ones the other way round. That number deserves to be chosen deliberately rather than falling out of whatever hardware you happened to buy.

Microsoft's own stipulation: if you build to the cell pattern, you run at least two cells. A single cell is not a cell, it is an ordinary installation with more elaborate management – the entire value of the pattern comes from having a second one to fail over to, migrate to and deploy to first.

3.Several regions and a control plane

Only now is the cell replicated: one in Europe, one in North America, one in Asia-Pacific. Each is an independent copy with its own database and its own object storage. No application data flows between them.

Regional cells: a global control plane sits above every region with an anycast edge, tenant catalogue, identity and CI/CD. Below it three identically built cells for Europe, North America and Asia-Pacific, each with its own IIS web nodes, MSSQL cluster and S3 storage. If one cell fails, the others are unaffected.
Figure 3: one cell per region – only control is global, never customer data.

What stays global is only what has to and that is deliberately little:

Building blockWhere it livesWhy
Tenant catalogueglobal, replicated into every regionAnswers one question only: which tenant belongs to which cell? Holds no business data.
Edge / WAF / TLSglobal, anycastAccepts the request close to the user and forwards it over the backbone.
Deployment & monitoringglobalOne artefact, many targets; one fleet view plus a view per cell.
Application (IIS/.NET)per cellHas to stand next to the database.
Database (MSSQL)per cellHolds the business data and the residency promise.
Documents (S3)per cellThe same residency promise as the database.
Backupsper cell, in the same legal areaA backup that leaves the region voids the residency promise.

That fixes the path of a request: the anycast point of presence accepts it, the catalogue answers "customer → region → cell" and the request lands in the right cell. AWS attaches a hard condition here that is worth taking seriously: the router is the only component with shared state, it must contain no business logic and the data plane must keep running when the control plane fails. If the tenant catalogue is unreachable, existing sessions must not stall – which is why the result of the lookup belongs in a session cookie rather than in every single request.

For the lookup itself there are two common routes. A CNAME per tenant (customer.app.example.com) allows a move between cells without changing the address, but creates DNS housekeeping per customer. A wildcard per region (*.eu.app.example.com) is operationally lean but writes the region into the address – making exactly the migration you will later need expensive. If you choose the wildcard, plan the migration path from the outset.

The ceiling almost everyone hits unprepared: Let's Encrypt issues 50 new certificates per registered domain per seven days – globally, across every account. If each tenant gets its own hostname under one domain, you cannot onboard more than roughly 50 new tenants a week. Renewals do not count: an order carrying exactly the same set of names as an existing certificate counts as a renewal and is exempt from the limit, and renewals coordinated through ARI are exempt from every limit. Reissuing the existing estate is therefore fine – what is capped is taking on new tenants. That stopped being a detail once six-day certificates arrived. If you plan to grow fast, spread tenants across several registered domains or use one wildcard certificate per region.

When you do not need this stage: Microsoft puts the practical floor for a real control plane at roughly ten tenants. Below that the tenant list is simply configuration and every piece of automation costs more than it saves. And a second region only pays off once law or distance forces it – it doubles baseline cost, rollout effort and failure modes.

What the application must learn

The painful part is rarely the infrastructure, it is the application. An ASP.NET application that spent years alone on one machine has picked up habits that break the moment a second node appears. The good news: it is a manageable, finite list.

State out of the process

Anything held in one node's memory is wrong from the second node onwards. The list is mercifully finite – and every line has a known remedy:

What breaksWhyRemedy
In-process session (InProc)The default; the session lives in the worker process and is invisible to every other node.Move it out (SQL Server or Redis) – better still, avoid sessions entirely.
Static fields, IMemoryCacheMicrosoft explicitly scopes IMemoryCache to a single server, or to servers using session affinity.A distributed cache; with HybridCache, know the limit (see below).
Uploaded files, generated PDFs, App_DataThey sit on local disk and simply do not exist on the neighbouring node.Into the object store.
Scheduled tasks, BackgroundService loopsThey run quietly on every node – the nightly job happens N times.A coordinating job framework or a distributed lock.

Background jobs deserve a closer look, because the common libraries behave differently. Hangfire is safe across multiple nodes with no extra work – the nodes coordinate through a distributed lock. Quartz.NET, by contrast, only clusters with its database-backed store, needs unique instance IDs and requires node clocks to agree to within one second. And a plain Windows scheduled task on the VM is simply wrong in a farm: it runs every job as many times as there are nodes.

The keys must be identical everywhere

This is the classic that users report as "it keeps logging me out". On .NET Framework the default AutoGenerate,IsolateApps produces a different key on every server – cookies from node A are useless on node B. The machineKey therefore has to be set explicitly and identically:

web.config.NET Framework – identical on every node
<!-- The same key material must exist on every node of the farm -->
<machineKey validationKey="<64 hex bytes>"
            decryptionKey="<32 hex bytes>"
            validation="HMACSHA256" decryption="AES" />

<!-- Session state out of process; better still: no session state at all -->
<sessionState mode="SQLServer" allowCustomSqlDatabase="true"
              sqlConnectionString="Data Source=sql-eu;Initial Catalog=AspState;…" />

On ASP.NET Core the same topic is called data protection. Two traps wait here and both strike silently. First, SetApplicationName() must be identical on every node. Second – and this is the nastier one – naming a key store silently switches off encryption of the keys at rest. If you move the key ring, you have to turn the protection back on explicitly:

C#Program.cs – a shared key ring
// Naming a key store silently disables encryption at rest — turn it back on.
builder.Services.AddDataProtection()
    .SetApplicationName("acme-suite")          // identical on every node
    .PersistKeysToDbContext<KeyRingContext>()
    .ProtectKeysWithCertificate(keyProtectionCert);

Trap: if you put the key ring in Redis without configuring persistence there, every Redis restart destroys all the keys – and with them every cookie ever issued. All users are logged out at once and nothing in the log looks like an error. Just as important: on ASP.NET Core the session cookie is itself encrypted with the data protection key. A shared session store therefore achieves nothing unless the key ring is shared too – a fault that tends to get misdiagnosed as "Redis is broken". On .NET Framework there is an extra condition: the IIS application paths must be named identically on every node.

Three things you cannot configure away

Not everything can be configured away and it is better to know that in advance:

Talk less and retry safely

Over distance every round trip counts. The most effective measures are unglamorous: eliminate N+1 queries in the ORM, batch queries, project results instead of loading whole entities and when in doubt use one stored procedure instead of ten separate calls. A retry policy for transient faults belongs with it – with the important caveat that an automatic retry can execute a non-idempotent command twice. Write operations therefore need an idempotency key before you switch retries on.

Time, language and numbers

Once customers sit worldwide, every local timestamp becomes a defect. Timestamps belong in the database in UTC and conversion into the tenant's time zone happens in the presentation layer. The same goes for number and date formats: the server has no culture, the request has one.

The port is not a precondition

A widespread fallacy runs: "modernise first, then scale". The opposite is true. An application on .NET Framework 4.8 can do everything described above – web pool, cell, several regions – as soon as state and keys are in order. .NET Framework 4.8 and 4.8.1 carry no end date at all: they are components of Windows and are maintained for as long as the operating system beneath them.

The port is still worth doing, just for different reasons – Linux containers instead of Windows images, lower running costs, a current runtime. It proceeds incrementally in the strangler-fig pattern: a YARP reverse proxy in front, the System.Web adapters in between and module by module moves to modern .NET while the rest keeps serving. Two points deserve honesty here: ASP.NET Web Forms has no migration path and never will get one; server-side WCF exists in modern .NET only through the successor project CoreWCF. The former .NET Upgrade Assistant has been deprecated; Microsoft now points at the modernisation agent in Visual Studio.

Two deadlines that press right now: .NET 8 and .NET 9 both lose support on 10 November 2026 – .NET 10 is the LTS release, supported to 14 November 2028. And the container base images on Windows Server 2022 (ltsc2022) leave mainstream support on 13 October 2026; anyone running Windows containers should schedule the move to ltsc2025. If you have been rolling out a VM per customer for years, audit the older ones too: Windows Server 2016 ends on 12 January 2027 and extended support for SQL Server 2016 already ran out in July 2026. If you are on .NET Framework 4.8 today the .NET deadlines do not apply to you – one more argument for not making the port a precondition of scaling.

The database layer

This is where the exercise succeeds or fails commercially. The technical options are plentiful, but they hang on the edition – and the edition hangs on the bill.

Inside a region: availability groups

The standard build per cell is an Always On availability group with a synchronous replica in the same region. An availability group holds up to nine replicas, several of which may commit synchronously – exactly how many depends on version and edition – worth checking against the build you actually run. Synchronous means every acknowledgement to the application waits for the replica, which only works over short distances. Microsoft names an upper bound for synchronous replication: a round-trip time below roughly two milliseconds – the order of magnitude between availability zones of one region, typically measured at 0.5 to 1.5 milliseconds. Across continents synchronous replication is out of the question; what remains there is asynchronous and asynchronous means data loss in a failure.

T-SQLReadable secondary with read-only routing
-- Reporting reads go to the secondary, writes stay on the primary.
ALTER AVAILABILITY GROUP [ag-eu-01]
  MODIFY REPLICA ON N'SQL-EU-02' WITH (
    SECONDARY_ROLE (
      ALLOW_CONNECTIONS = READ_ONLY,
      READ_ONLY_ROUTING_URL = N'TCP://sql-eu-02.internal:1433'
    )
  );

Between regions: asynchronous only, manual only

Coupling cells across regions lands you at distributed availability groups. It is worth knowing what that gives you – and what it does not: they support manual failover only, they have no listener of their own and read-only routing works on the primary side only. Microsoft explicitly recommends asynchronous commit for cross-site distributed groups. As a disaster recovery tool this is sound. As a foundation for "the customer works in both regions at once" it is not – and that is precisely why the governing rule says to keep every tenant in exactly one cell.

The edition is the real architectural decision

This is where the most expensive surprises are born. Standard edition offers only basic availability groups and their limits fit one-database-per-tenant about as badly as possible:

CapabilityStandard (basic AG)Enterprise
Replicas per group2up to 9 (several synchronous)
Databases per groupexactly 1many
Readable secondarynoyes
Backup on the secondarynoyes
Online and resumable index operationsnoyes

"Exactly one database per group" means 300 availability groups for 300 tenants. That is technically possible and operationally an imposition. The last row weighs just as much: online and resumable index operations are exactly the tools you use to roll schema changes across many tenant databases without downtime – and they are Enterprise features. If you need high availability on a pooled instance, budget for Enterprise or move to a managed database service and buy the capability there.

Two further points belong in the same calculation. Passive replicas are free only with Software Assurance – licences without SA carry no high availability or disaster recovery rights at all. And core licensing has a minimum of four core licences per physical processor or per virtual environment, sold in two-core packs. A tiny VM per customer is therefore never tiny in licensing terms.

Useful when pooling: contained availability groups from SQL Server 2022 replicate logins, users, permissions and Agent jobs at availability group level, through their own contained master and msdb databases. That removes the tiresome business of re-creating logins after every failover. SQL Server 2025 adds support for them inside distributed availability groups.

Schema changes across many databases

With one database per tenant every migration becomes a fleet operation. Two rules have proved themselves. First, versioned migrations rather than reconciliation at runtime – whether with EF Core migrations, DbUp or Flyway matters less than the fact that every database knows its own revision. Second, expand, then contract. A change is split across two releases: first the new shape is added additively so that the old and the new application version both run against it and only in the next release does the old shape disappear. That is the only way to roll a fleet out ring by ring without application and schema drifting apart.

Documents over S3

The object store is the least demanding part of this stack – provided three things are done right.

Documents do not travel through the application server

The most important lever: the web node issues only the permission, not the document. It generates a short-lived signed URL and the browser uploads to or downloads from the object store directly. The application server never sees the file, is never slowed by large attachments and needs no disk space for them.

C#A signed URL per tenant, valid briefly
// The key prefix carries the tenant; it is never taken from user input.
var url = s3.GetPreSignedURL(new GetPreSignedUrlRequest
{
    BucketName = cell.DocumentBucket,
    Key        = $"t/{tenantId}/{documentId}",
    Verb       = HttpVerb.GET,
    Expires    = DateTime.UtcNow.AddMinutes(5)
});

Two details that regularly cost time: signed URLs allow validity of up to seven days, but only with long-lived credentials – a URL signed under an assumed role dies with that role's session, typically after one or twelve hours. And signing does not replace the CORS configuration: the signature governs permission, the browser's same-origin rule is untouched by it.

One bucket per tenant, or a shared one?

Both are defensible and the boundary sits further out than many assume: the AWS default today is 10,000 buckets per account and can be raised to as much as a million – so a bucket per tenant holds up for a long time and it has the unbeatable advantage that access rights, encryption and retention are demonstrably separated per customer. Beyond a genuinely large tenant count, AWS instead recommends access points, or a shared bucket with one prefix per tenant secured by short-lived credentials scoped to exactly that prefix.

The decisive catch with a shared bucket: object lock and lifecycle rules apply at bucket level. Object lock in compliance mode cannot be undone and forces versioning on; lifecycle rules are capped at 1,000 per bucket. If you promise different retention or deletion periods per customer, you need separate buckets – that is not a matter of taste but a product characteristic.

Database and object store drift apart

A point that is almost always underestimated: the object store takes part in no database transaction. A rollback in SQL does not undo an object that has already been uploaded and an aborted upload leaves a row without a file. The way out is a two-phase write – object under a provisional key first, then the transaction, then the confirmation – plus a regular sweeper that finds orphaned objects and orphaned rows. An outbox pattern alone does not solve this.

The same applies one level up to replication between regions. S3 has been immediately consistent within a region since late 2020 – replication into another region is not. Those are two different promises and they get confused readily. If you need a deadline you buy it through replication time control, whose contractual promise is 99.9 per cent of objects within 15 minutes rather than 99.99. More importantly, replication silently skips a whole list of cases – objects that existed before the rule was created among them. That is exactly where the classic "the document is in the database but missing at the failover site" comes from.

European and self-hosted alternatives

If you would rather not hold the object store at a US hyperscaler, there is now a credible field to choose from: Scaleway and OVHcloud from France, IONOS, Hetzner and STACKIT from Germany, Exoscale from Switzerland. If you run it yourself, Ceph with the RADOS Gateway is hard to avoid – its multi-site replication is the strongest self-hosted answer for cross-region mirroring and for several releases it has supported sync policy per bucket. That maps cleanly onto "this customer's documents never leave Frankfurt, while that customer's are additionally mirrored to Paris".

Check this if MinIO is still in your plan: the open-source community edition of MinIO is no longer maintained – the public repository is archived and read-only. Its successor AIStor is a commercial product whose free tier is limited to a single node, which rules it out for distributed, multi-tenant operation. If MinIO sits in your design, verify that assumption now rather than at the next security advisory.

Data residency: where the law forces a region

Here architecture leaves technology behind. Three terms get mixed up routinely and mean different things:

The distinction is practical, because only the middle case compels a region of its own. The GDPR itself contains no localisation requirement – it restricts transfers to third countries (Chapter V). "We store exclusively in the EU" is therefore usually a commercial promise rather than a statutory demand. What genuinely forces a region falls into four categories:

Two widespread assumptions are out of date – carrying them into a design means planning one region too many: Saudi Arabia replaced the strict localisation default in its data protection law with a risk-assessment regime. And the Canadian province of British Columbia repealed its in-country storage requirement back in 2021; in Canada only the Nova Scotia rule still stands and that one is expiring too.

For the European part the picture has eased lately. Microsoft's EU Data Boundary has been complete since early 2025 and in January 2026 AWS brought its European Sovereign Cloud into service – a legally and operationally separate partition whose first region sits in Brandenburg. The United Kingdom received a renewed adequacy decision at the end of 2025 running to the end of 2031; for Norway and the other EEA states the GDPR applies anyway, so an EU cell is sufficient there. And mutual adequacy with Brazil has been in place since early 2026. Conversely, the European cloud certification scheme EUCS is still not adopted – anyone tendering for "sovereign" today works with contracts and evidence rather than a finished seal.

One point concerns the budget rather than the architecture: the EU Data Act has applied since 12 September 2025 and bars cloud providers from charging any switching fee from 12 January 2027 – including the egress charges for taking your data with you when you switch. Ordinary usage charges, the normal traffic price in day-to-day operation among them, are untouched. If you are planning a provider change or the relocation of a cell, that date is worth knowing.

The most frequently overlooked point: the residency promise covers every copy. A backup in another region, a centrally aggregated log carrying personal fields, an error report with payload data in a service outside the legal area – each of those voids the promise. That is exactly why the global layer holds control and never customer data.

Running the fleet

The difference between 20 and 200 instances is operations, not architecture. Four areas decide it.

Onboarding new tenants

A new customer must not be a project. Onboarding is a workflow that creates the database, the storage area, the DNS record, the certificate, the configuration and the catalogue entry in one pass – repeatable, abortable and with an explicit failure policy per step. Microsoft describes the decisive fork neatly as a choice between "tenant list as configuration" and "tenant list as data". Only the second yields a control plane; the first is a text file that will eventually be wrong.

The reverse direction is mandatory: a customer who leaves must demonstrably disappear – database, objects, backups, logs, catalogue entry. Without automated deletion you cannot credibly promise Article 17 of the GDPR.

SQLThe tenant catalogue – heart of the control plane
-- No business data lives here. Only: which tenant belongs to which cell.
CREATE TABLE dbo.TenantCatalog (
    TenantId    uniqueidentifier NOT NULL PRIMARY KEY,
    HostName    nvarchar(253)    NOT NULL UNIQUE,   -- customer.app.example.com
    CellId      varchar(32)      NOT NULL,          -- eu-central-01
    Residency   varchar(8)       NOT NULL,          -- EU | US | APAC
    Ring        tinyint          NOT NULL,          -- 0 canary … 2 broad
    Status      varchar(16)      NOT NULL           -- active | migrating | suspended
);

Deploying in rings

A fleet never receives the same release at the same time. Rollout runs ring by ring: a canary cell with internal or particularly cooperative tenants first, then a narrow wave, then the breadth. Between rings sits a wait long enough for faults to surface – usually at least one full working day in the relevant time zone. That presupposes that several application versions can work against the same schema, which is what "expand, then contract" above is for.

Probably the single biggest operational win of the year for Windows fleets: hotpatching through Azure Arc has been free since May 2026 and works on any substrate – including your own hardware and third-party hosters. On Windows Server 2025 it makes eight of the twelve months reboot-free. If you patch one VM per customer today, that halves the number of maintenance windows without changing a line of architecture.

Seeing what is going on

Monitoring has to know about cells. Every metric, log line and trace carries the cell and the tenant as an attribute, there is a dashboard per cell and an aggregated fleet view above it. The reason is uncomfortably simple: a single bad cell disappears into the fleet average. If you only watch the mean, you learn about the outage from the customer.

Backup and restore per tenant

The decisive capability is not the backup but the restore of a single tenant without touching the others. With one database per tenant that is elegant – a point-in-time restore concerns exactly one database. For the object store it takes versioning and object lock so that a compromised account cannot take the backups with it. And it takes practice: a restore procedure that has never been rehearsed is an assumption, not a procedure.

The licence arithmetic

In the end the unit-cost calculation decides which stage is economic. For this stack the structure is always the same:

Cost blockSilo (VM per customer)Pooled cell
Windows Server licenceper customerper node, shared
SQL Server licenceper customer, with a minimum per environmentper instance, shared
Computepaid for while idle toosized to the peak of the sum, not the sum of the peaks
Operational effortlinear in customer countlargely constant per cell
High availabilitypayable per customerpaid once per cell

The two middle rows are the actual lever. Idle time is the largest silent expense in the silo model: an instance for a customer with eight users is entirely unoccupied at night, at weekends and over lunch – and costs money regardless. And "peak of the sum" beats "sum of the peaks" convincingly from a handful of tenants onwards, because load peaks even out across customers and time zones.

For orders of magnitude on the licence side, using the open list prices for SQL Server 2025: a two-core pack costs roughly US$3,900 on Standard and about US$15,100 on Enterprise. Because of the four-core minimum per virtual environment, even a two-core VM per customer consumes two packs – so roughly US$7,900 per customer on Standard and US$30,000 on Enterprise, before Windows Server, compute, storage, backup and labour. Multiplied by the customer count, this is almost always the single largest line item of the whole exercise and the strongest reason to tackle stage 1 at all.

These are list prices, useful for comparing orders of magnitude rather than as a quote. A vendor running software as a service for third parties usually does not buy from the price list but through a service-provider rental programme or by the hour in the cloud, where the arithmetic looks different. The decisive point survives every model, though: the minimum purchase per environment makes small customers disproportionately expensive.

Calculate in unit cost per tenant per month, not in total cost. Only that figure reveals which customer size the silo can carry at all – and above which size a dedicated cell for one large customer becomes the right answer again. Offering both at once is not a contradiction: AWS calls this mixture of silo and pool the bridge model and treats it as the normal case rather than a compromise.

A decision framework

Seven questions that usually settle the direction quickly:

If the first two questions make you wince, you belong on stage 1. If question three does, you need shorter paths rather than necessarily a second region. Only when question four is answered with yes is stage 3 genuinely due.

Frequent questions

Does the application have to be ported to modern .NET before it can scale?

No. The most expensive mistake is treating the port as a precondition. An ASP.NET application on .NET Framework 4.8 runs across a multi-node web pool as soon as three things are true: identical machineKey values on every node, session state held out of process and no files written to local disk. That is days of configuration work, not months of porting. The port is worth doing on its own merits – Linux containers, running costs, security updates – and it can proceed incrementally through the System.Web adapters and a YARP reverse proxy while the existing application keeps serving traffic.

Is SQL Server Standard enough when every tenant has its own database?

For plain operation yes, for high availability usually not. One instance carries up to 32,767 databases, so the number of tenants is never the constraint. Standard edition, however, only offers basic availability groups: two replicas, exactly one database per availability group and no readable secondary. With one database per tenant that means one availability group per tenant, which becomes operationally untenable quickly. The tools for migrating schema across many databases without downtime – online and resumable index operations – are Enterprise features as well. If you need high availability on a pooled instance, budget for Enterprise or move to a managed database service.

When is a second region genuinely worth it?

When law or distance forces it, not when it looks like good architecture. Law forces it as soon as a customer requires contractually or statutorily that their data does not leave the country or economic area. Distance forces it once the application becomes noticeably sluggish across a continent – which is almost always down to the number of round trips per page rather than bandwidth. A second region doubles baseline cost, rollout effort and failure modes; anything short of that is solved more cheaply by terminating closer to the user and making fewer round trips.

One bucket per tenant or a shared bucket with prefixes?

A bucket per tenant holds up for a long time and is easy to audit: the AWS default today is 10,000 buckets per account and can be raised to as much as a million. Only beyond a genuinely large tenant count does a shared bucket with one prefix per tenant and short-lived credentials scoped to that prefix become more practical. The catch matters: object lock and lifecycle rules apply at bucket level; lifecycle rules are capped at 1,000 per bucket. If you promise different retention periods to different customers, you need separate buckets.

How does a tenant move from one region to another?

In the four steps AWS calls clone, flip, redirect and forget. First the state is copied into the target region and kept up to date, then the tenant goes read-only for a moment while the catalogue entry is switched to the new cell, then requests to the old address are redirected to the new one and only at the end is the data in the source region demonstrably deleted. That is why the move has to be designed for from the start: if the region is baked into customer URLs or into foreign keys, the tenant can never move again.

Sources

External sources, as of August 2026 (open in a new tab):

Architecture with Nokkela

From one-off to production line – without breaking operations

We accompany software vendors along exactly this route: analyse the existing solution, determine the stage that actually pays off and cut the rebuild so that live operations can absorb it. Vendor-independent, with data centres in Germany and Finland – and with European data sovereignty as the default rather than a surcharge.

Enquiry More about nokkela.systems

This article describes the situation as of August 2026 and is intended as general information. Product versions, licence terms and the legal position continue to develop – please check the points named here against current vendor information before deciding.