dotnet run is one of the first commands anyone learns, and one of the last they think about. It's worth understanding what it actually does, because the steps it hides are the same steps that determine how fast your service starts in production — and startup time has become a cost centre in ways it wasn't a decade ago.
What the Command Actually Does
dotnet run is a convenience that chains several distinct operations:
- Restore — resolve the dependency graph and make sure every package is on disk.
- Build — compile to IL, produce assemblies and the runtime configuration files that describe how to launch.
- Launch — start the host process, load the runtime, and hand control to your entry point.
Each is independently invokable, which matters the moment you're writing a Dockerfile or debugging why CI is slow. Knowing that restore is separate from build is what lets you cache them separately, and that single insight is worth more than most build optimisations.
Then the Runtime Takes Over
Once the process starts, a sequence runs before your first line executes:
The host reads the runtime configuration to work out which runtime version to load, then loads the CLR. Assemblies are resolved and loaded lazily as they're first needed. Every method is compiled from IL to machine code by the JIT the first time it runs — which is why the first request through a code path is reliably slower than the thousandth.
Then your code runs: configuration sources are read and merged, the DI container is built, and the middleware pipeline is assembled. In a large application the DI container build alone is measurable, particularly with assembly scanning.
Only after all of that does your service accept traffic.
Why This Matters More Than It Used To
For a service that starts once and runs for months, startup time is irrelevant. That describes fewer systems every year.
Containers make startup a scaling property. If you scale out under load, startup time is the delay between deciding you need capacity and having it. A service that takes 40 seconds to become ready cannot respond to a traffic spike; it responds to the spike you had a minute ago.
Rolling deployments multiply it. Slow startup means longer deploys, which means a longer window where you're running two versions, which is the window where version-skew bugs live.
Health checks and orchestrators care. Probe timings are tuned around startup. A service that drifts slower over time eventually starts failing its readiness probe on a busy node, which presents as a mysterious intermittent deploy failure rather than as a performance problem.
Where Startup Time Actually Goes
When I've looked at services that start slowly, the causes are usually mundane:
- Assembly scanning at startup. Reflecting over every type in every loaded assembly to wire up DI, validators, handlers, or mappings. Convenient, and it scales with the size of your codebase in a way nobody notices until it's slow.
- Eager work that should be lazy. Warming caches, opening connections, loading reference data — all before declaring readiness. Some of that genuinely must happen first. Much of it doesn't.
- Configuration from remote sources. A config server or secret store consulted at startup puts a network round trip, and someone else's availability, on your critical path.
- JIT on first request. Not startup exactly, but the same user-visible problem: the first requests after a deploy are slow.
What Actually Helps
Measure first. Log timestamps at host build, after DI container construction, and at first-request-served. Most teams have never looked and are surprised by which step dominates.
Prefer explicit registration over scanning in services where startup matters. More lines of code, materially faster, and it has the side benefit of making dependencies greppable.
Distinguish liveness from readiness properly. Work that genuinely must complete before serving traffic belongs before readiness. Everything else belongs in the background, after.
Consider ReadyToRun or AOT if startup is genuinely on your critical path. ReadyToRun precompiles to native code, cutting JIT cost at the price of a larger binary. AOT goes further with real constraints — no runtime code generation, and reflection-based libraries may not work. Both are meaningful commitments, worth it for short-lived or rapidly-scaled workloads, over-engineering for a service that starts twice a week.
Cache restore separately from build in CI and Docker. Dependencies change far less often than source. Copying project files and restoring before copying the rest of the source is the single highest-value line reordering in most Dockerfiles.
The Point
dotnet run is three operations and a runtime bootstrap wearing a trench coat. Knowing which step is which is what turns "the service is slow to start" from a vague complaint into something you can measure, attribute, and fix.



