Sitecore App Service Warm-Up Demystified
It’s well known the Application Initialization IIS module can assist in “warming up” an application. The problem this feature has always faced (for me, at least) is that its integration with Azure is not particularly well documented, and mostly limited to occasional forum posts that indicate deeper integration with the App Service load balancer without providing the detail.
That is, until now. Microsoft was kind enough to direct me to App Service Warm-Up Demystified by Michael Candido, a Principal Software Engineering Manager on the App Service team. This blog post goes into an incredible amount of detail about how the feature works in App Service. I’m not going to try to reproduce Michael’s blog post here, and I do consider it required reading if using this feature, but I’d like to summarise the feature’s behaviour in areas that directly affect Sitecore.
tl;dr
To reduce/remove any “cold” start delays to end users:
- Use deployment slots for blue/green deployments
- Use Application Initialization to warm up a specific resources for scale and maintenance events
- Use Advanced Application Restart to restart the application
Things to be aware of when using Application Initialization:
- Make sure your warmup calls can actually reach the target Sitecore “site” using specific hostName and/or URL rewrite rules
- Prevent caching of unintended absolute URLs (eg. “http://localhost”) by using relative URLs or setting the
targetHostname
andscheme
properties on the target site - Make sure your application can warm up (including app init URLs) in under 10 minutes
Overview
Generally speaking, the Application Initialization module simply requests a number of URLs sequentially when the application pool starts. It does so in the background, without interfering with traditional requests.
Azure App Service, however, provides additional support for this feature by preventing instances from receiving traffic until after all of the initialization URLs have been loaded sequentially.
This prevention applies in the following scenarios:
- Adding new instances via scale out (either manually or autoscale)
- Swapping deployments slots
- Instances being taken in and out for maintenance
- Advanced Application Restart (in “Diagnose and Solve Problems”)
It does not prevent requests from being sent to an instance in the following scenarios:
- Deployments (use deployment slots)
- Scaling up instances (there’s a feature request for this)
- Restarting your application via the “Restart” option (use Advanced Application Restart instead)
- Applications not running in Azure App Service (on-prem, Azure IaaS, etc)
Sitecore integration
There are a number of things to be aware of when using Application Initialization with Sitecore:
- Any response from an initialization URL is sufficient, even 302 (Redirect) or 500 (Error)
- All app initialization requests are sent using “localhost” unless configured otherwise
- All app initialization requests are sent using HTTP (ie. not HTTPS)
- The instance will be torn down if the application doesn’t restart in 10 minute (30 in some circumstances)
If not planned for, these nuances can result in instances receiving traffic too early because all of the requests resulted in not-found errors or redirects without actually warming anything up.
(Once receiving traffic, external requests will time out after 230 seconds and return a 502 status code. If you’re seeing this, it’s likely that your initialization URLs aren’t executing properly.)
Let’s look at these issues and how you can work around them.
Localhost
Assuming your target site isn’t configured to match localhost
, the request will likely result in a 404 (or a 302 to the Not Found page). There are a number of things to consider when fixing that:
- Whether to target the site via the host header or override the site via
?sc_site
- Whether to configure the target site on each initialization URL, or ‘globally’ via URL Rewrite
My personal preference is to rewrite ?sc_site
, since it’s portable between environments, using either Rewrite rule if there’s only one target site, or on each warmup URL if there are multiple sites being.
However, since your requirements may necessitate any combination of these options, I’ll provide configuration samples for each.
Using <applicationInitialization>
<system.webServer>
<applicationInitialization doAppInitAfterRestart="true">
<!-- Via site name -->
<add initializationPage="/?sc_site=veryexcellent" />
<!-- Via host name -->
<add initializationPage="/" hostName="veryexcellentsite.com" />
</applicationInitialization>
</system.webServer>
Using <rules>
NOTE: Modifying the host using a rewrite rule requires that you add HTTP_HOST
to system.webServer/rewrite/allowedServerVariables
via an applicationHost.xdt transform
Remember that rules are processed in order, so this will need to be first.
<rules>
<!-- Via site name -->
<rule name="Change warmup site" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTP_USER_AGENT}" pattern="^IIS Application Initialization Warmup$" />
<add input="{REMOTE_ADDR}" pattern="^127\.0\.0\.\d+$" />
</conditions>
<action type="Rewrite" url="{R:0}?sc_site=veryexcellent" appendQueryString="true" />
</rule>
<!-- Via host name -->
<rule name="Change warmup host" stopProcessing="true">
<match url=".*" />
<conditions>
<add input="{HTTP_USER_AGENT}" pattern="^IIS Application Initialization Warmup$" />
<add input="{REMOTE_ADDR}" pattern="^127\.0\.0\.\d+$" />
</conditions>
<serverVariables>
<set name="HTTP_HOST" value="veryexcellentsite.com" />
</serverVariables>
</rule>
</rules>
HTTP
HTTPS can’t be rewritten via a rule, but if you have an HTTP->HTTPS redirect you’ll need to inject an earlier rule that allows the warmup request to go through unsecured (the 302 won’t be followed, and will be treated as a success response).
NOTE: If you’re using a rule to apply the site/host, you can simply set stopProcessing="true"
on that and this rule won’t be required.
<rule name="No HTTP->HTTPS" stopProcessing="true">
<match url=".*" />
<conditions>
<add input="{HTTP_USER_AGENT}" pattern="^IIS Application Initialization Warmup$" />
<add input="{REMOTE_ADDR}" pattern="^127\.0\.0\.\d+$" />
</conditions>
<action type="None" />
</rule>
Cached Renderings with Absolute Links
If you have any cached renderings that use absolute links and you use AlwaysIncludeServerUrl
(either in those renderings, or globally), make sure you set the following in your site configuration so that your links aren’t cached as “http://localhost/*”:
targetHostname
scheme
(if using https)
Time limit
Finally, the application has 10 minutes to complete warmup before the VM is shut down. This might be as long as 30 minutes, but as it’s not reliable it’s best to aim for 10 minutes.
Individual warmup requests do not have a timeout applied so you can use them to “wait” for certain background processes to complete, if required.
I won’t cover startup optimisation as it’s a large topic on its own, but here’s two easy things that can help:
- Ensure the site is configured to use Roslyn (Microsoft.CodeDom.Providers.DotNetCompilerPlatform) as the cshtml compiler rather than the default. I’ve seen startup time drop by 3-4 minutes doing this
- Precompile views (for example, using RazorGenerator.MsBuild) and registering the assembly with Sitecore. Should should drop another 30-120 seconds from the startup time.
Debugging Warm-Up Requests
Warm-up requests sent by the Application Initialization module don’t appear in the IIS logs. They will be logged by Failed Request Tracing, but there’s also no way to filter this logging specifically for warmup requests.
One strategy is to create a dummy page that you can send ‘marker’ requests to:
<applicationInitialization>
<add initializationPage="/Init.aspx?phase=home" />
<add initializationPage="/?sc_site=awesome" />
<add initializationPage="/Init.aspx?phase=landing" />
<add initializationPage="/first?sc_site=awesome" />
<add initializationPage="/second?sc_site=awesome" />
<add initializationPage="/third?sc_site=awesome" />
</applicationInitialization>
This allows you to filter Failed Request Tracing to this page and you can review the logs to compare timestamps. If there is little to no time between marker requests, there’s probably errors occuring for the intervening requests.
You can also identify requests made by/during warmup:
- Warmup requests will have a user agent of
IIS Application Initialization Warmup
- Regular/external requests made while warmup is in progress will have a
APP_WARMING_UP
server variable set to1