The best .NET and ASP.NET Core hosting platforms in 2026
The most persistent myth in .NET hosting is that it's still a Windows question. It hasn't been for the better part of a decade — ASP.NET Core is a cross-platform, self-hosted web server that runs on Linux perfectly happily. Yet a surprising amount of the advice out there still routes you to IIS or Azure App Service by reflex, as though the only alternative were a Windows Server VM and a lot of regret.
The real evaluation axes are more interesting: which publish model you use, how much your process suffers during JIT warmup, how the garbage collector behaves in a memory-capped container, whether the platform keeps a Blazor Server circuit alive for an hour, and whether your background workers get a process or an invocation. This is a buyer's guide for someone deciding where to put an ASP.NET Core app — Minimal API, MVC, Blazor, or a Worker Service. I build PandaStack, which appears near the bottom and is flagged where it comes up.
What actually differs between .NET hosts
Three publish models produce three completely different artifacts
Before you compare platforms, decide what you're shipping. `dotnet publish` has three broad modes with almost nothing in common operationally.
**Framework-dependent** is the default: a small folder of DLLs that needs a matching .NET runtime already installed on the target. Smallest artifact, fastest build, and what every platform's .NET buildpack assumes. The catch is a version dependency you don't control.
**Self-contained** bundles the runtime into the output. Much larger, entirely independent of what the host has installed, and immune to somebody upgrading the platform's .NET version out from under you. Trimming shrinks it back down at the cost of anything that resolves types by reflection. This is the pragmatic choice on platforms with no first-class .NET support — you stop caring what they preinstalled.
**NativeAOT** compiles ahead of time to a native executable with no JIT and no runtime folder. Startup is dramatically better, memory footprint is lower, and the binary is small. What you give up is real: runtime code generation, `Assembly.LoadFrom`, and most reflection-heavy serialization unless it's source-generated. It suits Minimal APIs, gRPC services, and Worker Services far better than an old MVC app carrying a decade of dependencies. Audit every dependency first — the failure mode is a runtime exception in production, not a build error.
# 1. Framework-dependent (default). Smallest output; host needs the runtime.
dotnet publish -c Release -o out
# 2. Self-contained. Ships the runtime; doesn't care what the host has.
dotnet publish -c Release -r linux-x64 --self-contained true -o out
# 3. NativeAOT. One native executable, no JIT, no runtime folder.
# Requires clang + zlib on the BUILD machine, and a RID that matches
# the machine you're building for.
dotnet publish -c Release -r linux-x64 /p:PublishAot=true -o out
# What did you actually produce?
file out/MyApp # "ELF 64-bit ... dynamically linked" for AOT
du -sh out/ # the number that decides your image pull time
# ARM hosts are increasingly the cheap ones. The RID is the whole difference.
dotnet publish -c Release -r linux-arm64 --self-contained true -o outJIT warmup is the cold-start tax nobody budgets for
A freshly started ASP.NET Core process is not as fast as one that's been serving traffic for ten minutes, and the gap is larger than most people assume. Tiered compilation runs the first calls into a method as quick, unoptimised tier-0 code, re-jitting at tier-1 only once the runtime decides a method is hot. Add assembly loading, DI graph construction, EF Core model building, and the first-request cost of the JSON serializer, and your P99 for the first few hundred requests looks like a different application.
This is a hosting decision, not trivia. Where instances live for days, warmup happens once and nobody notices. Where the platform scales to zero or recycles instances aggressively, you pay it repeatedly, and it lands on real users. ReadyToRun is the standard mitigation: precompiled native code sits alongside the IL, so startup skips most tier-0 jitting. Binaries get bigger and the code is slightly less optimised than tier-1 would eventually produce, which is why you combine the two rather than replacing one with the other.
<!-- MyApp.csproj — the properties that decide your startup profile -->
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<!-- Precompiled native code alongside the IL: much less to JIT at
startup. Needs an explicit RID; costs you binary size. -->
<PublishReadyToRun>true</PublishReadyToRun>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<!-- Keep tiered compilation ON so hot paths still reach tier-1.
Quick JIT for loops helps startup-heavy code paths. -->
<TieredCompilation>true</TieredCompilation>
<TieredCompilationQuickJitForLoops>true</TieredCompilationQuickJitForLoops>
<TieredPGO>true</TieredPGO>
<!-- Drops ICU: smaller image, faster start, and a behaviour change.
Culture-sensitive comparison and formatting go invariant. Do NOT
set this if you format currency, sort user-visible strings, or
parse dates per-culture. -->
<InvariantGlobalization>true</InvariantGlobalization>
<!-- Source-generated JSON: mandatory for AOT, and a startup win
everywhere else because there's no reflection-based warmup. -->
<JsonSerializerIsReflectionEnabledByDefault>false
</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>
</Project>Server GC in a memory-capped box is the classic surprise
Here is the bug report I've seen more than any other .NET hosting problem: 'the app doesn't leak, but the container keeps getting OOM-killed, and memory usage looks insane for what it does.' The answer is almost always the garbage collector doing exactly what it was configured to do.
Server GC allocates a separate heap and a dedicated GC thread per logical core, and deliberately lets garbage accumulate before collecting, because on a dedicated machine with plenty of RAM that's throughput-optimal. Workstation GC uses a single heap and collects more eagerly. ASP.NET Core enables Server GC by default, on the reasonable assumption that a server app wants server behaviour.
Put that default inside a container capped at a modest memory limit, on a host reporting a large number of cores, and you get many heaps each willing to grow before collecting — high resident memory that looks like a leak and isn't. The runtime does read cgroup limits, but container-aware is not the same as tuned for your workload. For a small service, Workstation GC is frequently the better answer and nobody ever tries it.
# /etc/systemd/system/myapp.service
# Kestrel serving directly, behind nginx doing TLS. No IIS anywhere.
[Unit]
Description=MyApp (ASP.NET Core)
After=network.target
[Service]
WorkingDirectory=/srv/myapp
ExecStart=/srv/myapp/MyApp
Restart=always
RestartSec=5
User=myapp
# Kestrel binds here; nginx proxies to it.
Environment=ASPNETCORE_URLS=http://127.0.0.1:5000
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
# --- GC tuning: the part that decides your memory bill ---
# Workstation GC. Smaller footprint, lower throughput. Try it first on
# small services before you reach for a bigger instance.
Environment=DOTNET_gcServer=0
# Or keep Server GC but bound it. Hex bytes; this is 1 GiB.
# Environment=DOTNET_gcServer=1
# Environment=DOTNET_GCHeapHardLimit=0x40000000
# Cap heaps instead of one per logical core on a 64-core host:
# Environment=DOTNET_GCHeapCount=4
# Trade some CPU for a materially smaller RSS:
# Environment=DOTNET_GCConserveMemory=5
[Install]
WantedBy=multi-user.target
# systemctl daemon-reload && systemctl enable --now myapp
# journalctl -u myapp -fBlazor Server is a WebSocket app wearing a web framework's clothes
Blazor Server renders components on the server and ships DOM diffs to the browser over a SignalR circuit. That circuit is a WebSocket that stays open for as long as the tab is open, with per-user component state living in server memory behind it. It is the single biggest reason a .NET hosting decision goes wrong.
Three things to verify before committing a Blazor Server app anywhere. WebSockets end to end — if the proxy silently downgrades to long polling, the app still works and feels awful. Maximum connection duration and idle timeout — a proxy that caps connections terminates circuits mid-session, and because Blazor reconnects gracefully, users experience this as 'the page randomly loses what I typed' rather than as an error anyone reports usefully. And sticky sessions — the negotiate handshake and the resulting connection must land on the same instance.
Blazor WebAssembly is the mirror image and much easier to host: the output is static files any CDN serves, so the interesting question moves entirely to whatever API those files call. Blazor Web Apps with per-component render modes are the awkward middle — treat them as a Blazor Server hosting problem and you'll be right.
Worker Services and IHostedService need a process, not an invocation
The .NET generic host makes background work so easy that mature apps accumulate it almost by accident: an `IHostedService` draining a queue, a `PeriodicTimer` loop, a Hangfire or Quartz scheduler, a cache with a refresh task. None of it belongs to a request, and all of it stops the moment a platform freezes your container between invocations.
So the question for a request-scoped platform is not 'does .NET run here' — it does — but 'is my process CPU-scheduled when no request is in flight'. If not, your timers don't fire and your queue consumer stops consuming, silently. Standalone Worker Services have the same requirement more obviously, and a surprising number of platforms treat a workload with no HTTP port as a configuration error rather than a valid service type. The general shape of this trade-off is at /blog/background-workers-alongside-web-apps.
Where your database lives, and the SQL Server assumption
A lot of .NET estates assume SQL Server, and that assumption quietly narrows the platform list — most modern app platforms offer managed Postgres and MySQL and nothing else. Npgsql plus EF Core makes Postgres a genuinely first-class .NET target, so a greenfield app has little reason to prefer SQL Server. Migrating an existing schema full of SQL Server idioms is real work, though, and if you're staying on it that constraint dominates everything else here. Check region co-location early: a cross-region round trip erases every millisecond ReadyToRun bought you.
The realistic options
Azure App Service
Let me be unfashionable and say the obvious thing: for a lot of .NET shops, Azure is genuinely the right answer, and picking it is not a failure of imagination. App Service is first-party, with the deepest .NET integration of anything here — deployment slots, managed identity that removes connection strings from your config entirely, profiling that understands .NET internals, and a support relationship with the people who write the runtime. Slots with warm-up are also the cleanest answer to the JIT warmup problem anyone offers.
The trade-offs are what you'd expect. It's the most vendor-shaped option on this list, so leaving later is a project rather than a config change, and the plan model rewards understanding it. If you're already committed to another cloud, adopting Azure for one app buys you a second bill and a second identity system.
Azure Container Apps
The other Azure answer, and often the better one now: managed containers on a Kubernetes-shaped substrate without you operating Kubernetes, with scale-to-zero, KEDA-driven event scaling, and Dapr if you want it. It suits a .NET microservice estate well, and workers scaling on queue depth is exactly what it was built for. The .NET-specific caveat is scale-to-zero plus JIT warmup — publish ReadyToRun at minimum, and check whether a minimum replica count is the honest answer for user-facing endpoints. Also confirm what happens to a Blazor circuit during scale-in.
AWS Elastic Beanstalk and ECS Fargate
Two generations of the same answer. Beanstalk is the older, more managed one, with a .NET Core on Linux platform and a reasonable landing spot if your organisation already lives there. ECS on Fargate is the modern default: bring a container, AWS runs it without you managing instances. Long-running processes are normal, so Worker Services and Blazor circuits are fine in principle — you configure the ALB for WebSockets, set idle timeouts that don't sever them, and enable stickiness. The honest caveat is that neither is a PaaS in the Heroku sense: you will write task definitions, target groups, health check paths, and security group rules. Fair if you're already fluent in AWS, unreasonable if this is your first deploy.
AWS Lambda (with the .NET runtime)
Lambda has a managed .NET runtime and a genuinely good developer story — annotations that generate the plumbing, a hosting shim that lets a Minimal API run behind API Gateway with modest changes, and NativeAOT support that addresses the cold-start objection head-on. If your workload is honestly request-scoped and bursty, this is cheap and effective, and pairing it with AOT or SnapStart-style mitigation is the difference between '.NET is slow on Lambda' being true and being folklore.
The mismatch is structural, not performance-related. No long-lived process means no `IHostedService`, no background timers, no in-memory cache surviving between invocations, no connection pool you can rely on reusing — and .NET apps accumulate all of those because the generic host makes them trivial to write. Blazor Server is simply out. Use Lambda for stateless APIs and event handlers, not as a general home for an app with a life of its own.
Google Cloud Run
Cloud Run has moved a long way from its original request-scoped model: it runs ordinary containers, supports WebSockets, and offers configurations where instances stay alive rather than being throttled between requests. An ASP.NET Core container runs there without ceremony, and inside an existing GCP footprint it's a sensible use of infrastructure you already have.
Three .NET-specific things to verify, all historically real constraints: maximum request duration, which also bounds how long a Blazor circuit survives; whether CPU is allocated outside request handling, because a throttled instance won't run hosted services or timers; and how aggressively instances are recycled, because every recycle is another JIT warmup on the critical path. Read the current limits yourself — this is exactly where a vendor's constraints change between writing and reading.
Fly.io
Real VMs you control, launched from a container image, with private networking and the option to run close to users. For .NET this is a clean fit: a long-lived process, WebSockets as ordinary traffic, and no ambiguity about whether your background worker is scheduled. Running a separate Worker Service alongside the web app is straightforward rather than a workaround. You're closer to the infrastructure than on a classic PaaS, and multi-region introduces database-locality decisions a single-region app never faces. Check the current state of their managed Postgres rather than assuming.
Railway
The nicest first hour on this list. Connect a repo, get a service; add Postgres from a menu; variables reference each other so the connection string simply appears; per-branch environments make review apps genuinely pleasant; and their build system handles .NET without a Dockerfile in the common case. Long-running processes are the default, so hosted services and Blazor circuits behave. The trade-offs are the usual convenience-first ones: less machine control, usage-based pricing that rewards knowing what your app consumes, and fewer adjacent managed services than a hyperscaler.
Render
Heroku-shaped and unexciting in the best sense: long-lived web services, managed Postgres, background workers and cron as first-class object types rather than clever hacks, and a repo-level blueprint describing the whole stack. That worker support matters for .NET specifically, because it gives a standalone Worker Service somewhere obvious to live instead of being smuggled into the web process. The caveat is that .NET is a container-or-buildpack citizen here, not a first-class runtime with vendor-specific tooling.
Northflank
The most infrastructure-shaped of the developer platforms: build pipelines, jobs, managed databases, and services in one product, with meaningfully more control over build and runtime than the simpler options. For a .NET estate that is several services plus workers plus scheduled jobs — which describes a lot of enterprise .NET — that breadth is the point. The trade-off is more surface area to learn than Railway or Render, and a smaller community to ask when you're stuck.
DigitalOcean App Platform
The straightforward managed-container option inside a cloud that also sells droplets, managed Postgres, and object storage, with pricing that's easy to reason about. Deploy a container, get a long-running service, attach a managed database in the same region, receive one bill. There's no .NET-specific awareness, so you'll usually maintain your own Dockerfile — which, given the publish-model discussion above, may be what you wanted. A good fit if you're already there, or value the escape hatch to plain droplets.
A VPS with systemd, Kestrel, and nginx
Genuinely underrated, and the unit file above is the whole thing. Kestrel is a real production web server; the traditional advice to always front it with a reverse proxy is about TLS termination, static files, and request buffering rather than about Kestrel being untrustworthy. A self-contained publish, a systemd unit, and nginx doing TLS is a complete deployment that will run for years, costs the least of anything here, and gives you total control over GC settings. You own patching, TLS renewal, backups, zero-downtime deploys, and the pager. Worth it with one or two services and someone who enjoys owning servers; a bad trade with twelve and nobody who does.
PandaStack
Mine, so apply the appropriate discount. PandaStack is git-driven app hosting on Firecracker microVMs: connect a repo, push to deploy, each deploy builds into a fresh microVM that takes traffic once health checks pass. The process model is the useful part for .NET — every app is a real long-running VM with its own kernel and its own RAM, so `IHostedService`, `PeriodicTimer` loops, Hangfire, and Blazor Server circuits behave the way the generic host expects rather than the way a request-scoped platform allows. Managed Postgres 16 is a create-and-attach environment variable away, provisioned in 30–90 seconds.
The honest caveat first, because it's specific. .NET is not one of the pre-warmed runtimes in the `base` template the way Node, Python, Go, and Bun are, so the SDK gets installed at build time on every deploy unless you bake a custom template with it already present. That also makes a self-contained or NativeAOT publish a notably good fit: build the artifact once, ship something that doesn't care what runtime the guest has, stop paying the install. If a vendor-maintained .NET buildpack is what you want, Azure and the container PaaS options above are being more honest with you than I would be.
The substrate is the genuinely different part. Machines boot by restoring a snapshot rather than booting cold — roughly 179ms at p50 and 203ms at p99 end to end, with a ~49ms restore step, against about 3s for a true first cold boot. That makes scale-to-zero honest rather than aspirational, which is exactly where the JIT warmup discussion lands: the VM comes back fast, so the remaining cold-start cost is your process's warmup, which ReadyToRun or NativeAOT exists to solve. Forking a running machine takes 400–750ms on the same host or 1.2–3.5s across hosts, which makes an environment per branch cheap enough to be a habit.
The other thing microVMs are good for, and a reason a .NET team might reach for one specifically, is running code you don't fully trust — building an external contributor's pull request, or letting an agent restore packages. `dotnet restore` executes third-party MSBuild targets, which is a fact more C# developers should be alarmed by; full treatment at /blog/sandbox-untrusted-dotnet-nuget-install. The API for that is the sandbox one, not the app-hosting one:
from pandastack import Sandbox
# A disposable microVM to build and smoke-test a .NET service — a PR from
# outside the org, or an agent-authored change. ttl_seconds reaps it if we
# crash before cleanup, so nothing leaks when the harness dies.
with Sandbox.create(template="base", ttl_seconds=900) as sbx:
# Pin the SDK the way the repo already declares it. On `base` the .NET
# SDK is installed at build time rather than pre-warmed — the honest
# trade-off of a runtime this platform doesn't bake in.
sbx.filesystem.write("/workspace/global.json", '{"sdk":{"version":"10.0.100"}}')
setup = sbx.exec(
"curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 10.0",
timeout_seconds=600,
)
if setup.exit_code != 0:
raise RuntimeError(f"SDK install failed:\n{setup.stderr}")
# restore + build run third-party MSBuild logic inside a guest kernel
# with no host credentials anywhere in the environment.
build = sbx.exec(
"cd /workspace && ~/.dotnet/dotnet publish -c Release "
"-r linux-x64 --self-contained true -o out",
timeout_seconds=900,
)
print("build exit:", build.exit_code)
print(build.stdout[-2000:])
# Pull the artifact back through the API rather than a host mount.
if build.exit_code == 0:
binary = sbx.filesystem.read("/workspace/out/MyApp")
with open("MyApp", "wb") as f:
f.write(binary)
print(f"pulled {len(binary)} bytes")
sbx.kill()
# The VM and everything the build did to it are gone here.Where PandaStack is a poor fit: if you need SQL Server rather than Postgres, if you want a vendor-maintained .NET buildpack, or if your organisation is already deep in Azure with managed identity wired through everything. Those are real reasons to pick something else, and I'd rather say so than pretend otherwise.
The one-line version
- Azure App Service — Process model: long-lived, with deployment slots and warm-up. Best for: .NET shops wanting first-party integration, managed identity, runtime-aware profiling. Watch out for: the most vendor-shaped option here.
- Azure Container Apps — Process model: managed containers, scale-to-zero, event-driven scaling. Best for: a .NET microservice estate with queue-driven workers. Watch out for: scale-to-zero plus JIT warmup — publish ReadyToRun, and check circuits on scale-in.
- AWS Beanstalk / ECS Fargate — Process model: long-lived containers or instances you configure. Best for: teams already fluent in AWS who want normal process semantics. Watch out for: it's infrastructure, not a PaaS — ALB WebSocket config and stickiness are yours.
- AWS Lambda — Process model: request-scoped invocations, frozen in between. Best for: genuinely stateless APIs and event handlers, especially with NativeAOT. Watch out for: no hosted services, no timers, no Blazor Server, no state outliving a request.
- Google Cloud Run — Process model: containers with request-oriented scaling and configurable always-on CPU. Best for: .NET services inside an existing GCP footprint. Watch out for: max request duration bounds WebSocket life, and CPU throttling stops background work.
- Fly.io — Process model: real VMs you control, with private networking. Best for: long-lived services, separate worker processes, being close to users. Watch out for: closer to infrastructure than a classic PaaS.
- Railway — Process model: repo-connected long-running services with per-branch environments. Best for: the fastest path from git to a running ASP.NET Core app. Watch out for: less machine control and fewer adjacent managed services.
- Render — Process model: Heroku-shaped containers with first-class workers and cron. Best for: one ASP.NET Core app plus a standalone Worker Service and managed Postgres. Watch out for: .NET is a generic citizen here, not a first-class runtime.
- Northflank — Process model: services, jobs, and build pipelines with real configurability. Best for: a multi-service .NET estate with scheduled jobs. Watch out for: more surface area to learn, smaller community to ask.
- DigitalOcean App Platform — Process model: managed containers beside droplets and managed Postgres. Best for: one vendor, predictable billing, an escape hatch to plain VMs. Watch out for: no .NET awareness — you own the Dockerfile.
- VPS + systemd + Kestrel behind nginx — Process model: your process, your machine, your GC flags. Best for: one or two services, lowest cost, total control. Watch out for: you own patching, TLS, backups, and the pager.
- PandaStack — Process model: git-driven deploys onto long-running Firecracker microVMs with per-app kernel isolation. Best for: Blazor circuits and Worker Services needing a real process, genuine scale-to-zero, cheap environment-per-branch, untrusted .NET builds. Watch out for: .NET isn't pre-warmed in the base template, so the SDK installs at build time unless you bake a template or publish self-contained.
Pick by situation
- Already on Azure, or need SQL Server and managed identity → Azure App Service, or Container Apps if you're container-shaped. This is not a consolation prize; it's frequently the correct answer.
- One ASP.NET Core app plus a Worker Service, and you want it boring → Render, or DigitalOcean App Platform if you're already there.
- Optimising for developer velocity and per-branch preview environments → Railway.
- It's a Blazor Server app → anything with a long-lived process, sticky sessions, and no aggressive proxy idle timeout. Verify by opening a circuit and leaving it alone for an hour.
- Honestly request-scoped and bursty → AWS Lambda with NativeAOT. Check you have no hosted services first; that's the constraint people discover late.
- Organisational gravity puts you on AWS or GCP → ECS Fargate or Cloud Run, with WebSocket timeouts and CPU-outside-of-requests verified first.
- You have someone who enjoys owning servers → a VPS, systemd, and nginx. Cheapest here, and it will outlive several platform migrations.
- You need per-app kernel isolation, staging that costs nothing while idle, or a safe place to build code you don't trust → PandaStack, mine, with the build-time SDK install priced in.
- Someone proposes running Blazor Server on a function platform → send them /blog/websocket-apps-persistent-connections-hosting before the architecture review.
The short version
For most ASP.NET Core apps the platform matters less than five checks. Pick a publish model deliberately and know what it needs from the host. Mitigate JIT warmup with ReadyToRun or NativeAOT if your platform recycles instances or scales to zero. Look at the GC configuration before a bigger instance size. Confirm your process stays CPU-scheduled between requests, or accept that your background work doesn't exist. And if a Blazor circuit is involved, verify WebSockets, idle timeouts, and stickiness with a real hour-long session rather than a smoke test.
Get those right and Azure, AWS, Cloud Run, Fly, Railway, Render, Northflank, DigitalOcean, a VPS, and PandaStack all work fine. Get them wrong and you'll spend a fortnight convinced Blazor is flaky, when what's flaky is a load balancer idle timeout you never read.
Frequently asked questions
Do you still need Windows or IIS to host ASP.NET Core?
No, and you haven't for years. ASP.NET Core ships with Kestrel, a cross-platform managed web server, and the framework runs natively on Linux — which is where the majority of new .NET deployments now go. IIS still exists as a reverse proxy in front of Kestrel on Windows, and legacy ASP.NET Framework applications genuinely do require Windows, but that is a different product from ASP.NET Core. If you're building anything new, treat Linux as the default target and choose Windows only when a specific dependency forces it: a COM component, a Windows-only third-party library, or an authentication scheme tied to Active Directory in a way you can't replace.
Should you use NativeAOT for a web app?
It depends on what your app does more than on how much you want fast startup. NativeAOT eliminates JIT entirely, which produces dramatically better cold starts and a smaller memory footprint — genuinely transformative for Minimal APIs, gRPC services, and Worker Services on platforms that scale to zero or recycle instances. But it forbids runtime code generation, dynamic assembly loading, and most reflection-based serialization unless it's source-generated, and plenty of mature libraries rely on exactly those. Full MVC with Razor views has historically not been a good AOT candidate, and EF Core support has evolved but still needs verification against your specific usage. Audit your dependency list first; a failure here surfaces as a runtime exception in production rather than a build error, which is the worst possible time to find out.
Why does my .NET container use so much memory when the app isn't leaking?
Almost always Server GC doing what it's designed to do. ASP.NET Core enables Server GC by default, which allocates a separate heap and GC thread per logical core and deliberately lets garbage accumulate before collecting, because that's throughput-optimal on a dedicated machine. Inside a container with a modest memory limit on a host reporting many cores, that produces high resident memory that looks exactly like a leak. The runtime does read cgroup limits, but reading them isn't the same as being tuned for your workload. Try DOTNET_gcServer=0 for Workstation GC on small services, or keep Server GC and bound it explicitly with DOTNET_GCHeapHardLimit and DOTNET_GCHeapCount. Measure before and after rather than guessing; the right answer differs a lot between a JSON API and a service doing large allocations.
Can you host Blazor Server on serverless or edge platforms?
Structurally, no. A Blazor Server circuit is a SignalR WebSocket that stays open for the life of the page, with per-user component state living in server memory behind it, so any platform that caps request duration, freezes the container between invocations, or routes reconnections to a fresh instance will terminate sessions mid-use. Because Blazor reconnects gracefully, you don't see errors — you see users reporting that pages randomly lose their state, which is much harder to diagnose. Container platforms that keep a genuinely long-running process, including some Cloud Run configurations, can work fine. Function-per-request platforms cannot. Blazor WebAssembly is the opposite case entirely: it compiles to static files that any CDN serves, and the hosting question moves to whatever API those files call.
How do you reduce ASP.NET Core cold start on a platform that scales to zero?
Attack it in layers, because there are two separate costs. The platform's cost is getting a machine or container running at all, which you control by choosing a platform whose start path is fast — snapshot-restore substrates and prewarmed instance pools both address this, and a minimum replica count sidesteps it entirely at the price of paying for idle. Your process's cost is JIT warmup, assembly loading, DI graph construction, and EF Core model building, and that's what ReadyToRun or NativeAOT is for. Beyond publish settings, source-generate your JSON serialization, avoid touching the database during startup where you can, and consider a warm-up request against a real endpoint before the instance enters rotation — deployment slots on some platforms do this for you. Measuring the two costs separately is the important part; teams routinely tune the wrong one.
Keep reading
49ms p50 cold start. Fork, snapshot, and scale to zero.