Why default IIS configuration slows down .NET applications
A default IIS installation gives you a working configuration — and that is exactly its limitation: it is designed for one server, one application, and a moderate request flow.
As soon as the number of concurrent requests grows, behavior becomes non-linear: the server keeps responding, but response time grows in steps, and at peak some connections drop without any useful error in the application.
The first bottleneck is the request queue. A request does not wait indefinitely: an application pool has a limit on concurrent handlers and a separate limit on requests waiting in the queue.
When both are exhausted, a new request is rejected before it ever reaches the code: in the logs this looks like "server busy", even though the application never learned about the error.
A typical sign is rising response time with almost zero CPU load: the bottleneck of the application server here is not computation but waiting.
The second is worker process recycling. A pool is recycled on a schedule, by the number of processed requests, and by a memory consumption threshold.
Every such recycle resets data prepared in memory, loses session state, and forces the assembly to warm up again, so the first requests after startup are many times slower than the rest.
The third is interrupted long-running operations: exports, reports, and file processing do not run to completion if they fall into the pool recycle window or into idle time.
Failures under load here do not look like a service crash but like partially generated files and repeated runs of the same task.
We set queue and concurrency limits to match the load profile, move recycling into controlled windows, move state out of process memory, and add warm-up. Below we look at how this is addressed under real load.
What a well-tuned application server configuration delivers
Configuring Microsoft IIS is not just "install the role and publish the site".
A measurable effect comes from a combination of solutions: a placement scheme that distributes applications across isolated pools, request queue limits, and releasing new versions without dropping active connections.
Consistent response time at peak load
A separate pool for each group of applications prevents a single heavy request from occupying all processing threads.
Environment preloading and warmed-up connections eliminate the spike on the first requests, while queue and timeout limits turn overload into predictable behavior instead of an avalanche of failures.
Under load, response time grows linearly with the number of requests instead of jumping in steps once the server hits a limit.
The share of failed requests drops: instead of accumulating endlessly in the queue, the server honestly limits intake and returns a correct response to those it accepts.
Fewer failures when releasing a new version
Updates happen by switching between two prepared slots: the new version comes up alongside, warms up, and takes traffic only after a successful health check.
If the check fails, requests stay on the previous slot — there is no downtime during the update, and returning to the working version takes seconds.
This changes the class of failures. Instead of "everything went down and stayed down for a long time", you get a local failure of one pool that does not affect neighboring applications on the same server. Application resilience stops depending on who pressed publish and how.
IIS configuration schemes: shared pool, isolation, separate node
The placement scheme is fixed before sites are configured and determines what happens on failure: whether one application goes down or the entire environment. The difference between the options lies in the blast radius and in the amount of manual work during updates.
| Scheme | When it fits | Limitations |
|---|---|---|
| Shared pool | Services of one environment on a common runtime version | A memory leak or a crash of one site takes down the whole pool |
| Separate pools | Different runtime versions, dedicated accounts and memory limits | More worker processes, harder to consolidate logs |
| Separate node | Load at the server's limit, strict data isolation | Updates, certificates, and monitoring are duplicated |
| External load balancer | Fault tolerance, maintenance without downtime | Session state is moved outside, health checks are required |
When a single shared pool is enough
A shared pool is justified when services in the same environment run on a common runtime version and shared libraries. Memory is saved — there is a single worker process — and warmed-up assemblies return the first response faster.
The condition is a unified release schedule: a restart for the sake of one site affects its neighbors.
Pool isolation and a separate node: the trade-offs
A separate pool localizes crashes and memory leaks and provides dedicated accounts and limits. A separate node is needed when a single server's headroom is exhausted, and load balancing in front of the nodes adds fault tolerance and maintenance without downtime.
The price is moving session state outside and setting up health checks. Each step to the right reduces mutual impact and increases the number of nodes you have to watch.
Implementation sequence: from audit to handover to support
The implementation stages follow a fixed sequence: capture the current state, design the placement, configure parameters and permissions, test under load, and hand over with runbooks.
The order is not a formality — it prevents editing pools and bindings on a live server when there is no snapshot of the original configuration and nowhere to roll back to.
Audit and placement scheme
- Configuration audit: we export site configuration files, record the composition of application pools and their startup modes, memory limits, bindings, account permissions, and the set of installed modules. The result is a map of discrepancies between the current environment and the application's requirements.
- Application placement scheme: we determine the number of sites and pools, the distribution of applications between them, and the placement of logs and temporary files. A separate pool per application provides isolation — a memory leak or failure in one does not drag down its neighbors, and a scheduled restart affects only it.
- Target parameters: startup mode, warm-up, memory limit with auto-restart, pool recycle schedule, logging level, static content caching and compression rules, and request size limits.
Configuration, load testing, and handover
- Permissions and access: a dedicated pool account, the minimum required permissions on the application and log directories, disabling unused modules, and closing service paths off from external access.
- Load testing: we run a profile close to peak and look at response time, request queue, and behavior during pool recycling and cold start. We separately test node failure — the load balancer must divert traffic without losing active sessions.
- Handover to support: deployment and rollback instructions, the log collection procedure, a change checklist, and a configuration backup retention scheme. We hand this over to the on-call shift along with an analysis of typical failures and the boundaries of responsibility.
What the team is left with after the work is delivered
Work is considered complete not at the moment the application opens, but when your team can update, roll back, and recover it on its own.
That is why, along with the working configuration, we hand over artifacts that let the on-call engineer act without waiting for a contractor's response.
Documentation and runbooks
- Node placement diagram: which applications live on which servers, where the load balancer is, where the certificates are, and which nodes can be taken out of service simultaneously.
- IIS configuration files — application pools, bindings, memory and execution time limits — with an explanation of why each value was chosen and what happens if it is changed.
- Access policies: service account permissions, authentication, address restrictions, and separation of administrator and on-call rights.
- Update and rollback runbook: the procedure for taking a node out of load balancing, warm-up, verification, and reverting to the previous artifact if a release fails.
- Instructions for the on-call shift: what to look at in the logs, which counters to consider normal, and how to tell application degradation from a node problem.
Deployment scripts and backups
- Node installation and update scripts — idempotent: re-running them brings the server to the same state rather than breaking the configuration.
- Backups of configuration, certificates, and logs with mandatory restore verification on a separate node.
- An acceptance checklist that your team runs on its own — we compare results and close any discrepancies.
Case study: how we reduced failures during application updates
An application on Windows Server was updated by stopping the pool: the engineer unloaded the site, copied the files, and brought the pool back up.
Any configuration error or a file still held open by the process meant downtime until morning and a post-mortem with the on-call shift.
Before the work
The pool started without warm-up, so the very first request after a release triggered compilation and opened database connections — timeouts regularly dropped at this step.
Rollback was kept "from memory": the previous set of files sat next to the current one with no version, and recovery took as long as the release itself. The on-call shift logged 8–11 incidents per month, two-thirds of them right after updates.
What changed after
We split two pools on the same site and moved switching into a pre-production environment: traffic is diverted only after the application has warmed up and a health check on a separate address has passed.
We enabled preloading and disabled the idle pool reset, and the previous version of the directory is kept with a label — if the check fails, the binding reverts automatically.
| Metric | Before | After |
|---|---|---|
| Releases with errors | 3 out of 10 | 1 out of 20 |
| Recovery after a failed release | 40–60 minutes | 2–3 minutes |
| On-call incidents per month | 8–11 | 1–2 |
| Downtime when rolling out a new version | 5–10 minutes | no downtime |
Releasing a new version became a routine operation performed by the on-call engineer according to the runbook: prepare, warm up, switch, verify. Recovery time dropped because rollback is a single binding switch rather than a repeated manual deployment.
How does a shared application pool differ from isolated ones?
An application pool in IIS is a process isolation boundary. Each pool corresponds to a separate worker process with its own runtime instance, account, and limits.
As long as applications live in a shared pool, they share a single process: one request queue, one set of restart settings, one memory and thread budget.
Hence the impact on neighboring applications. A memory leak in one service eats into the memory available to the others; long synchronous calls occupy threads from the shared pool, and the queue grows for everyone.
Diagnostics get harder: counters, traces, and logs from several applications are mixed within a single process.
A process crash is the most painful scenario. An unhandled exception or a failure in a third-party library takes down the entire worker process, and it comes back up from scratch: all applications in the pool restart, in-memory sessions and unfinished requests are lost.
If rapid-fail protection kicks in, the pool is disabled entirely — even stable neighbors that were never at fault become unavailable.
An isolated pool limits the scale of the consequences: a failure affects one application while the rest keep serving requests. But you pay for this in resource consumption — each pool is a separate process with its own runtime and baseline memory footprint.
Dozens of pools on a single server squeeze the available memory and lengthen the cold start after a restart.
A practical rule: isolate applications with different accounts and permissions, different runtime modes, different restart schedules, and unstable external components.
It makes sense to keep low-load services with identical requirements together, reinforcing the boundaries with CPU limits and rapid-fail protection — that way a failure of one application does not turn into a failure of the node.
Technical concerns before starting the work
Before starting work, clients usually clarify three things: whether the application code will have to be touched, what will happen to the already running server, and who is responsible for the system after handover. We answer them directly — this resolves most questions before the estimate.
Do we need to change the application code?
IIS configuration is a hosting-level concern: application pools, bindings, directory permissions, handlers, environment variables, timeouts, and memory limits.
Code changes are needed only where the application hardcodes paths or ports — we point out such places and agree on minimal changes. We do not touch the application's logic or architecture.
What happens to the running server, is downtime required?
We migrate in stages: first we bring up the target configuration alongside the current one and test it, then we switch traffic, and only after that do we decommission the old environment.
The unavailability window is seconds for switching bindings, not hours of downtime; with a second site, the switch is invisible to users. We prepare the plan for returning to the previous configuration in advance.
How do you verify the configuration and what remains after handover?
We run the application through a checklist: pool startup, static content delivery, routing, behavior on restart and on abnormal termination of the worker process, logs, and error tracing. We separately verify access permissions so the application does not run with excessive rights.
After handover we provide the pool and binding diagram, restart policies, and the procedure for typical operations — your engineers can run the system themselves, while we remain on support under an agreed runbook.
Let's discuss the task and come back with a work plan
So that the timeline estimate is based on facts rather than assumptions, start by describing the current situation and constraints: what is running now, where the bottleneck is, and what cannot be changed.
The more specific the input data, the more accurate the work plan and the fewer revisions at the start of the project.
It is worth including with your request:
- a placement diagram: how many servers, where the load balancer is, how traffic is distributed between nodes;
- the current IIS configuration — application pools, number of worker processes, bindings, timeouts, request queue limits;
- a description of peaks: when the maximum load arrives, how many times it exceeds the average, and how response time behaves;
- logs: Windows Server events, IIS logs, response codes, and slow request traces for a representative period;
- the release and rollback procedure: how the application gets onto the server, how long the old version stays live, and what you do on failure;
- external dependencies: certificates, domains, authentication services, calls to databases and third-party services;
- constraints: maintenance window, availability and access separation requirements, and the client's security rules.
Based on this data, we will come back with a work plan, a timeline estimate, and a project kickoff procedure. If some information is not available, describe at least the outline — we will capture the rest on site once access is agreed.







