🌐 IIS

IIS URL Rewrite and ARR: Reverse Proxy Setup

📅 July 21, 202610 min readITVedas

Configure IIS as a reverse proxy with URL Rewrite and ARR: server farms, load-balancing algorithms, session affinity, and header handling.

ADVANCED
⏱ 10 min read
Prerequisites:
Key Facts
  • URL Rewrite and ARR are separate downloads from the Web Platform Installer or IIS.net, not built into IIS by default
  • ARR's proxy function is disabled by default and must be turned on explicitly under "Proxy Settings" at the server level
  • ARR supports round robin, weighted round robin, least current requests, least response time, and request hash load-balancing algorithms
  • Session affinity in ARR uses an encrypted cookie (ARRAffinity) rather than source-IP hashing by default

URL Rewrite vs ARR: What Each Module Does

IIS ships with neither URL Rewrite nor Application Request Routing (ARR) installed by default. Both are separate downloads distributed through the Web Platform Installer or as standalone MSI packages from iis.net, and they solve different problems that only overlap once you combine them.

URL Rewrite is a pattern-matching and rewriting engine. It inspects incoming requests against a set of rules built from conditions (URL path, query string, HTTP headers, server variables) and, when a rule matches, either rewrites the URL internally, issues an HTTP redirect, or — critically for this article — rewrites the request to point at a different URL entirely, including one on a remote host. Used alone, URL Rewrite is commonly seen doing things like stripping .aspx extensions, forcing HTTPS, or normalizing trailing slashes. None of that requires ARR.

ARR is what turns IIS into an actual reverse proxy and load balancer. It adds three things URL Rewrite cannot do on its own: a proxy module capable of forwarding a request to an upstream server and relaying the response back to the client, the concept of a server farm (a named pool of backend servers), and load-balancing logic to decide which farm member handles each request. ARR also integrates with URL Rewrite by exposing farms as valid rewrite targets and by auto-generating a default rewrite rule when you create a farm.

In practice the two modules are used together: URL Rewrite decides whether and where to route a request, ARR decides which backend server receives it and manages the connection. You can install URL Rewrite without ARR for pure rewriting/redirecting work, but you cannot do reverse proxying with ARR alone — ARR depends on URL Rewrite rules to trigger routing to a farm.

Installing URL Rewrite and ARR

Both modules must be installed at the server level (not per-site) before they appear in IIS Manager. Download URL Rewrite 2.1 and Application Request Routing 3.0 from the official Microsoft/IIS download pages and run the installers, or use the Web Platform Installer command line if it's still available in your environment. A reboot is not typically required, but an IIS restart (iisreset) is recommended after installation so the modules register cleanly in the IIS pipeline.

After installation, confirm both modules are present by opening IIS Manager and selecting the server node (not a site). You should see a URL Rewrite icon and an Application Request Routing Cache icon in the server-level feature list. If you only see them at the site level or not at all, the install did not register correctly — reinstall rather than trying to work around it, since ARR specifically needs to hook into the server-level request pipeline to proxy connections.

Verify module versions match your IIS and OS architecture (x86 vs x64) — a mismatched ARR install is a common cause of the proxy silently failing to route while URL Rewrite rules appear to match correctly in the trace logs.

Enabling the ARR Proxy Function

This is the step most new IIS-as-proxy setups miss. Installing ARR does not enable proxying — the proxy function ships turned off. At the server node in IIS Manager, open Application Request Routing Cache, then click Server Proxy Settings in the Actions pane. Check Enable proxy and apply.

Until that checkbox is set, any URL Rewrite rule that rewrites to an http:// or https:// URL will fail with a 404 or a rewrite loop, because IIS has no proxy handler registered to actually forward the request off-box — URL Rewrite will happily rewrite the internal URL, but nothing picks it up and sends it anywhere. This setting is global to the server (it lives in applicationHost.config under system.webServer/proxy), so on a shared or multi-site IIS box, enabling it affects every site unless you scope proxy behavior per rule.

The same Server Proxy Settings screen also exposes timeout, response buffer size, and — importantly for this setup — the Reverse rewrite host in response headers option, which rewrites Location, Content-Location, and Set-Cookie headers coming back from the backend so redirects and cookies reference the proxy's public hostname instead of the internal backend name.

Configuring a Basic Reverse Proxy Rule

With both modules installed and proxying enabled, the simplest reverse proxy is a single inbound rule that matches everything and rewrites to a backend server. Add this to the web.config of the site acting as the proxy front end:

<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="ReverseProxyToBackend" stopProcessing="true">
          <match url="(.*)" />
          <conditions>
            <add input="{HTTP_HOST}" pattern="^app\.example\.com$" />
          </conditions>
          <action type="Rewrite"
                  url="http://backend-farm/{R:1}"
                  logRewrittenUrl="true" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

Here backend-farm is not a hostname resolved via DNS — it's the name of an ARR server farm defined on the same server, and ARR intercepts the rewrite to that name and applies load balancing across the farm's members. If you point the rewrite at a single backend hostname instead of a farm name, IIS proxies to that one server directly with no load balancing, which is valid for a simple pass-through proxy but defeats the purpose of pairing URL Rewrite with ARR.

When you create a farm through IIS Manager's Server Farms node, ARR offers to auto-generate this rule for you at the server level (in applicationHost.config), targeting http://farmname/{R:1}. That generated rule is a reasonable starting point but is scoped globally — for anything beyond a single-site proxy, move or copy the logic into the specific site's web.config and add host or path conditions so unrelated sites on the same server aren't affected.

Server Farm Configuration and Load-Balancing Algorithms

A server farm is created under the server node's Server Farms item, where you give it a name and add one or more backend servers by address (and optionally a non-default port). Each farm member can be individually weighted, taken offline for maintenance without deleting it, or assigned to a backup pool that only receives traffic when primary servers are unavailable.

ARR exposes several load-balancing algorithms per farm, configured under the farm's Load Balance settings:

For most reverse-proxy-in-front-of-a-web-app scenarios, Least Current Requests is the pragmatic default; reach for weighted round robin only when farm members are genuinely unequal in capacity.

Health Checks and Session Affinity

A farm's Health Test settings (under the farm node) let ARR periodically send a test request to each backend — a specific URL, expected response match, and interval/timeout — and automatically remove a server from rotation if it stops responding correctly. Configure this against a lightweight endpoint (a dedicated health-check route that checks dependencies like the database, not just a static file) so ARR reacts to real application failures, not just process liveness. Without a configured health test, ARR relies only on failed proxy connections to detect a dead server, which reacts more slowly and can route a burst of requests to a server that's already down.

For applications that store session state in-process (classic ASP.NET session state, in-memory caching per instance, anything not externalized to a shared store like Redis or SQL Server), a single client needs to keep hitting the same backend server for the life of its session. ARR provides this via Session Affinity, also configured per farm. When enabled, ARR issues an encrypted cookie named ARRAffinity (and, over HTTPS, an additional ARRAffinitySameSite cookie depending on version) on the first response, encoding which backend server handled that client. Subsequent requests carrying the cookie are routed back to the same server as long as it remains healthy.

Affinity is a workaround, not a substitute for stateless backend design — if the affinitized server goes down, ARR falls back to load-balancing the client to a different server and that client's in-memory session is lost. For anything beyond small internal tools, externalizing session state is the more robust fix; affinity buys time for legacy applications that can't be refactored immediately.

Preserving Client IP and Host Headers

Once IIS is proxying, every request the backend sees originates from the proxy server's IP address unless you explicitly forward the original client's information. Backend applications that log client IPs, apply IP-based rate limiting, or make authorization decisions based on source address will silently break — everything appears to come from one internal IP — unless this is addressed.

ARR automatically adds an X-Forwarded-For header containing the original client IP when proxying, appending to the header if one already exists from an upstream hop (such as a load balancer or CDN in front of IIS). This behavior is controlled by the X-Forwarded-For proxy setting at the server level and is on by default in current ARR versions, but it's worth verifying under Server Proxy Settings rather than assuming — backend code must be written to read this header explicitly, since the underlying TCP connection IP will still be the proxy's.

The Host header is a separate concern: by default, URL Rewrite/ARR preserves the original Host header sent by the client rather than substituting the backend server's hostname, which matters when the backend application generates absolute URLs, validates the request against expected host bindings, or serves multiple sites and dispatches based on Host. If the backend needs to see the original host explicitly regardless of any changes upstream, add a serverVariables section to the rule that sets HTTP_X_FORWARDED_HOST, and ensure that server variable is allowed by adding it to the allowedServerVariables collection under the rewrite module's configuration — attempting to set an unlisted server variable fails silently at request time rather than throwing a configuration error.

Common Gotchas and Troubleshooting

The single most common failure is forgetting to check Enable proxy in ARR's server-level proxy settings after installing the modules — rewrite rules match correctly, the rewritten URL looks right in Failed Request Tracing, but nothing is actually forwarded and the client gets a 404. Check this first whenever a proxy rule appears to do nothing.

A second frequent issue is a rewrite rule that targets a farm name but the farm has no healthy members, or the health test URL is wrong and marks every member as down — this produces a 502 rather than a 404, which is a useful distinguishing signal during troubleshooting.

Rewrite loops occur when a rule's match pattern is broad enough to also match the rewritten URL on a subsequent internal pass; scope match conditions tightly (by host header or path prefix) and set stopProcessing="true" on proxy rules so later rules in the chain don't re-evaluate an already-rewritten request.

Finally, Failed Request Tracing (FREB) is the most reliable diagnostic tool for this setup — enable it for status codes 400-599 on the proxy site, reproduce the failing request, and inspect the trace for the exact rewrite rule matched, the resulting URL, and whether ARR's proxy module engaged at all. This distinguishes a rewrite-rule authoring problem from a farm health/connectivity problem far faster than guessing from the client-facing error alone.

Key Takeaways

  • URL Rewrite handles the rule matching and rewriting logic; ARR supplies the server farm, load balancing, and proxying engine that URL Rewrite hands requests off to
  • A rewrite rule alone will not proxy traffic anywhere until "Enable proxy" is checked in ARR's server-level proxy settings
  • Server farms abstract a pool of backend servers behind a single farm name that URL Rewrite rules target as a rewrite URL
  • Health checks and affinity cookies keep a farm resilient and, when needed, keep a given client pinned to one backend for session state
  • Without forwarding X-Forwarded-For and preserving the Host header, backend applications lose the real client IP and see every request as coming from the proxy