Logit.io
IIS logs: 5xx errors, sub-status and HTTPERR rejections
← Back to blog
11/15/2026 · 28 min read

IIS logs: 5xx errors, sub-status and HTTPERR rejections

Logit.io Team
Logit.io Team
Technical Content Team

Last updated 11/15/2026

Log ManagementHow To GuidesOpensearch

The Microsoft IIS pack in the Logit.io Dashboard Library installs one OpenSearch Dashboards view, Logit.io IIS web activity, that reads two Windows logs at once: the W3C access log each IIS site writes and the HTTPERR log that HTTP.sys writes for requests it rejects before IIS ever sees them. It fills when Filebeat ships both through its iis module to your Logs stack, which parses each line into status codes, IIS sub-status codes, client addresses and HTTP.sys reason phrases. The one setup choice that decides whether it works is which W3C fields you log: the defaults and the full standard set parse, but adding just the byte counts makes every line fail to index.

This guide is for teams running ASP.NET, ASP.NET Core, classic ASP or static sites on Windows Server who want to see the 500.19 after a config change, the 503s from a stopped application pool and the clients HTTP.sys threw away, without logging on to each server and opening u_ex*.log in Notepad. It covers how the two logs differ, which W3C field sets the parser reads, how to ship both with Filebeat on Windows, how to record the real client behind a load balancer, how to prove the whole path with two requests, how to read sub-status and Win32 codes, how to turn the signals into alerts and what to check when the panels look wrong.

Contents

What the Microsoft IIS pack shows

The pack is listed as Microsoft IIS in the Dashboard Library and installs a single dashboard, Logit.io IIS web activity. It reads the filebeat-* index pattern with @timestamp as the time field, opens on the last 24 hours and refreshes every 60 seconds. Every panel starts from the same Lucene base query:

event.module:iis AND agent.type:filebeat AND event.dataset:(iis.access OR iis.error)

The access panels narrow that to event.dataset:iis.access, and the HTTP API error panels narrow it to event.dataset:iis.error. The eleven panels are laid out top to bottom in the order you'd use them:

PanelWhat it countsField it reads
HTTP requestsAccess log recordsevent.dataset
Server errorsAccess log records with a 500 to 599 statushttp.response.status_code
HTTP API error entriesHTTPERR recordsevent.dataset
Last event receivedNewest IIS event in the range@timestamp
Web log activity over timeAccess and HTTPERR records, stackedevent.dataset
Requests by response statusTop five exact status codeshttp.response.status_code
Requests by methodTop five HTTP methodshttp.request.method
HTTP API errors by reasonTop five HTTP.sys reason phrasesiis.error.reason_phrase
Activity by log datasetAccess versus HTTPERR shareevent.dataset
Requests to investigateLatest 4xx and 5xx requestsTime, method, status, user, client and host
HTTP API error detailLatest HTTPERR recordsTime, method, status, reason, client and host

Two things are deliberately missing. There's no response time panel, because only the access log has a duration and mixing it with HTTPERR records would mislead. There's no map either, because IIS servers behind a load balancer log private addresses, which can't be geolocated. You can add both to a copy of the dashboard once your data supports them, and this guide shows the queries.

Two logs from two layers of the same server

IIS doesn't accept connections itself. HTTP.sys, the kernel-mode HTTP listener in Windows, takes every connection first, checks that the request is well formed, works out which site and application pool it belongs to and queues it for the worker process. Only then does IIS run the request and write a line to the site's W3C log. If HTTP.sys refuses the request, or the application pool can't take it, IIS never hears about it and the W3C log has no line. HTTP.sys writes it to its own log instead:

W3C access logHTTPERR log
Written byIIS, one folder per siteHTTP.sys, one folder per server
Default locationC:\inetpub\logs\LogFiles\W3SVC<site-id>\C:\Windows\System32\LogFiles\HTTPERR\
File namesu_ex<yymmdd>.loghttperr<n>.log
Filebeat datasetiis.accessiis.error
Typical contentEvery request IIS handled, including its 4xx and 5xxMalformed requests, bad Host headers, stopped or overloaded app pools, idle and slow connections
Time zoneUTCUTC

This is why the pack has two error signals. Server errors counts 5xx responses that IIS sent, usually your application failing. HTTP API error entries counts what HTTP.sys rejected or dropped, which includes the worst outage of all: an application pool that has stopped, where every user gets a 503 and the W3C log goes quiet. If you only ship the access log, that outage looks like a sudden drop in traffic.

A request reaches HTTP.sys first, which either rejects it and writes the HTTPERR log or passes it to the IIS site and application pool that writes the W3C access log. Filebeat ships both logs to the Microsoft IIS dashboard

Choose W3C fields the parser can read

When Filebeat's iis module ships a line, your Logs stack's Logstash sends it to an IIS access or IIS error ingest pipeline before it's stored. The access pipeline uses fixed patterns that expect W3C fields in IIS's own column order. IIS lets you tick any combination of fields in IIS Manager, and not every combination fits a pattern.

We ran lines built from each common field selection through the same IIS access pipeline definition and Beats index template that current Logit.io Logs stacks use, on a local OpenSearch node. The results fall into three groups, and one of them is a trap:

Fields selected in IIS ManagerResult
The IIS 8.5 and later default set (15 fields, shown below)Fully parsed: time, server address, method, path, query, port, user, client, user agent, referrer, status, sub-status, Win32 status and duration
The IIS 7 default set (no cs(Referer))Fully parsed, without the referrer
Either default set plus an X-Forwarded-For custom fieldFully parsed, and the first forwarded address lands in network.forwarded_ip
All 22 standard fieldsFully parsed, plus site, server name, host header, HTTP version and request and response bytes
All standard fields except Server Name, Server IP and Protocol VersionFully parsed, as above without those three
Default set plus Host, Site Name or Protocol VersionStored, but not parsed: the document has error.message and no HTTP fields
Default set without Time Taken, or all fields except CookieStored, but not parsed
Default set plus Bytes Sent, with or without Bytes ReceivedRejected: the line is never stored

The last row needs explaining, because it fails silently. IIS writes sc-bytes and cs-bytes just before time-taken. The default pattern still matches, but it reads the response size as the duration and the next number as an optional forwarded client address. That field is mapped as an IP address, a number such as 412 isn't one, and OpenSearch refuses the whole document with a mapper_parsing_exception. Logstash then drops it, or writes it to the dead letter queue if you've enabled one. Your server is logging, Filebeat is shipping and nothing appears.

So pick one of two configurations and keep every site on the same one:

  • Keep the default fields. Simplest, and enough for every panel in the pack. Add X-Forwarded-For as a custom field if you're behind a load balancer.
  • Tick all 22 standard fields. Choose this if you want response sizes, the site and host header per request. Again, add X-Forwarded-For if you need it. Note that Cookie is part of the set and can contain session identifiers, so think about who can read filebeat-* before you turn it on.

The fastest way to see what a server is actually writing is the #Fields header IIS puts at the top of every log file. Run this in PowerShell on the web server:

Get-ChildItem C:\inetpub\logs\LogFiles\W3SVC*\*.log |
  Sort-Object LastWriteTime | Select-Object -Last 1 |
  Get-Content -TotalCount 4

On a default IIS 10 server the fourth line is:

#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status time-taken

With all 22 standard fields it reads:

#Fields: date time s-sitename s-computername s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs-version cs(User-Agent) cs(Cookie) cs(Referer) cs-host sc-status sc-substatus sc-win32-status sc-bytes cs-bytes time-taken

Anything else, check against the table before you rely on it. The format must also be W3C. The older IIS and NCSA formats and custom log providers don't match any pattern, and a site set to log to ETW events only writes no file at all. HTTPERR has a fixed format you can't change, and the error pipeline reads both the older layout and the newer one that adds a streamid column, so there's nothing to choose there.

Ship both logs with Filebeat on Windows

Find every site's log folder

The module's default access path is C:/inetpub/logs/LogFiles/*/*.log, which covers every site that logs to the default folder, one W3SVC<site-id> subfolder each. Many teams move logs to a data drive, and some move one site but not others. List where each site really logs, and in what format:

Import-Module WebAdministration
Get-ChildItem IIS:\Sites | Select-Object name, id,
  @{n='logFolder'; e={[Environment]::ExpandEnvironmentVariables($_.logFile.directory)}},
  @{n='format';    e={$_.logFile.logFormat}}

Every format should say W3C. Note the id as well: the folder for site 2 is W3SVC2, and that's also what IIS logs as the site name if you choose all fields, so it's worth keeping a note of which number is which site.

Configure the iis module

Start with the IIS integration guide in your Logit.io account. Choose Install Integration first, which prepares your stack for this source, then install Filebeat for Windows as the guide shows. From the Filebeat folder in an elevated PowerShell prompt (C:\Program Files\filebeat if you used the zip, or the folder the installer opened), enable the module:

cd 'C:\Program Files\filebeat'
.\filebeat.exe modules enable iis

Then edit modules.d\iis.yml. Both filesets are disabled by default, so you must enable them. Listing the paths explicitly makes the file obvious to the next person, and lets you add any site that logs elsewhere. Use forward slashes:

- module: iis
  access:
    enabled: true
    var.paths:
      - C:/inetpub/logs/LogFiles/*/*.log
      - D:/IISLogs/*/*.log             # any site you moved, from the list above
  error:
    enabled: true
    var.paths:
      - C:/Windows/System32/LogFiles/HTTPERR/*.log

Folders that don't exist are skipped, and the module already ignores the # header lines and .gz files. Set the output in filebeat.yml. Copy the block from the integration guide while signed in, because it fills in your stack's Logstash host and Beats SSL port:

filebeat.config.modules:
  path: ${path.config}/modules.d/*.yml
  reload.enabled: false

output.logstash:
  hosts: ["your-logstash-host:your-beats-ssl-port"]
  loadbalance: true
  ssl.enabled: true

Test and start the service:

.\filebeat.exe test config
.\filebeat.exe test output
Start-Service filebeat
Get-Service filebeat

test output should end with talk to server... OK. The Filebeat service runs as Local System, which can read both log folders. Beats data sent through your stack's Logstash lands in daily filebeat-* indices, which is what the pack reads. If you have renamed the index in your Logs stack, the pack won't find your data.

Use the module rather than a plain filestream input on the same folders. The module sets event.module:iis and tells your stack which ingest pipeline to use. A plain input sends raw lines that are never parsed into HTTP fields, and the pack never sees them.

Record the real client with X-Forwarded-For

Behind Azure Application Gateway, an AWS load balancer, a CDN or Application Request Routing, c-ip is the address of the device in front of IIS. Every request then seems to come from a handful of private addresses, and the client column in Requests to investigate is no help.

IIS 8.5 and later can log a request header as a custom field. In IIS Manager, open the server or site, choose Logging, then Select Fields, then Add Field and set Field Name to X-Forwarded-For, Source Type to Request Header and Source to X-Forwarded-For. Or set it for every site at once:

Add-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' `
  -Filter 'system.applicationHost/sites/siteDefaults/logFile/customFields' `
  -Name '.' -Value @{logFieldName='X-Forwarded-For'; sourceName='X-Forwarded-For'; sourceType='RequestHeader'}

IIS appends custom fields after the standard ones, which is exactly where the access pipeline looks. The first address in the header goes into network.forwarded_ip. Two caveats. source.ip still holds the load balancer's address, because that's what c-ip says, so the pack's tables keep showing it: add network.forwarded_ip as a column in a copy of the dashboard. And the header is only trustworthy if your load balancer sets or appends it itself. A client can send its own X-Forwarded-For, so don't use the value for access decisions.

IIS W3C and HTTPERR log lines travel from the Windows server through the Filebeat iis module and your stack's Logstash into the IIS access and error ingest pipelines, which add status, sub-status, client and reason fields before the documents land in filebeat-* indices and feed the IIS web activity dashboard

Check both logs end to end in five minutes

You don't need to wait for real traffic or a real outage. Two requests from the web server itself prove both halves, one per log:

curl.exe -s -o NUL -w "%{http_code}`n" "http://localhost/logit-iis-test-$(Get-Date -Format yyyyMMdd)"
curl.exe -s -o NUL -w "%{http_code}`n" -H "Host: bad host" "http://localhost/"
netsh http flush logbuffer

The first should print 404 and write a W3C line for a path nothing real uses. If your default site only binds HTTPS, use https://localhost/ and add -k. The second should print 400. A space isn't allowed in a host name, so HTTP.sys rejects the request before IIS sees it and writes an HTTPERR line with the reason Hostname. IIS and HTTP.sys buffer their log writes, so netsh http flush logbuffer pushes both lines to disk now instead of in a minute or so.

Open Discover in OpenSearch Dashboards, choose filebeat-*, set the query language to Lucene and the time picker to the last 15 minutes, and search:

event.dataset:iis.access AND url.original:\/logit-iis-test-*

Open the document and check that http.response.status_code is 404, iis.access.sub_status is 0, iis.access.win32_status is 2 (file not found) if IIS served the 404 itself or 0 if your application did, event.outcome is failure and source.ip is ::1 or 127.0.0.1. The path is in url.original, exactly as IIS logged it in cs-uri-stem. Then the HTTPERR half:

event.dataset:iis.error AND iis.error.reason_phrase:Hostname

That document should have http.response.status_code 400 and destination.port 80, plus the client's source.ip and source.port. If either document has error.message and no HTTP fields, it arrived but didn't parse: compare the server's #Fields line with the table above. If the 404 never arrives at all and Filebeat's log shows no errors, look for the Bytes Sent trap first.

Next, confirm the field types the panels need, using the OpenSearch API on your stack. The endpoint and stack API key are on the Endpoints page in your stack settings, and the guide to connecting to your stack's OpenSearch API covers both authentication modes. In API key mode:

curl -s "https://<stack-id>-es.logit.io/filebeat-*/_field_caps?fields=event.dataset,http.response.status_code,http.request.method,iis.access.sub_status,iis.access.win32_status,iis.error.reason_phrase,url.original,source.ip,network.forwarded_ip,event.duration&apikey=<stack-api-key>&pretty"

You want http.response.status_code, both IIS status fields and event.duration as long, source.ip and network.forwarded_ip as ip and the rest as keyword, all aggregatable. On filebeat-*, string fields are mapped as keyword directly, so you group on iis.error.reason_phrase, not iis.error.reason_phrase.keyword as you would on logstash-*. Index templates and mappings on Logit.io stacks are managed for you (the index templates guide shows how to read them). The default Filebeat template is applied when an index is created, so if a type looks wrong, fix it in the pipeline or the shipper rather than editing the live mapping; existing daily indices keep their first type until they age out.

Finally, count what the Library will see. Detection looks for recent documents in filebeat-* with event.module of iis, and needs event.dataset and http.response.status_code to be aggregatable. Save this as iis-detect.json:

{
  "size": 0,
  "query": { "bool": { "filter": [
    { "range": { "@timestamp": { "gte": "now-24h" } } },
    { "term": { "event.module": "iis" } }
  ] } },
  "aggs": {
    "datasets": { "terms": { "field": "event.dataset" } },
    "hosts":    { "terms": { "field": "host.name", "size": 50 } },
    "unparsed": { "filter": { "exists": { "field": "error.message" } } }
  }
}

Then run it from any machine with curl, including Windows, where it's curl.exe:

curl -s -H "Content-Type: application/json" --data-binary @iis-detect.json \
  "https://<stack-id>-es.logit.io/filebeat-*/_search?apikey=<stack-api-key>&pretty"

Expect an iis.access bucket, an iis.error bucket once anything has been rejected and one hosts bucket per web server. If a server you expect is missing from hosts, its Filebeat isn't shipping. If unparsed is more than a handful, one server or site is using a field set the parser doesn't know.

Install the pack and read it top down

Open your Logs stack's card in your Logit.io account and choose Dashboard Library. Search for Microsoft IIS. Once matching events have arrived, the pack shows a Data detected badge. If you've only just started shipping, press Rescan stack. Tick the IIS web activity dashboard, choose Install dashboards and open it in OpenSearch Dashboards a minute or two later. The Dashboard Library guide covers the permissions you need and what gets installed, and the post on how Dashboard Library detection works explains the badge in more depth.

Read the dashboard in this order:

  1. Last event received. On a site with steady traffic, a timestamp more than a few minutes old means collection has stopped, not that your users went home. Remember the minute or so of log buffering before you worry about a gap.
  2. Server errors next to HTTP requests. Any 5xx at all deserves a look, but the trend matters more than the total.
  3. HTTP API error entries. A steady trickle is normal on a public site. A jump, especially with a fall in HTTP requests at the same time, is the stopped-pool pattern described below.
  4. Web log activity over time. The stacked chart shows whether the access and HTTPERR series move together. When HTTPERR rises as access falls, requests are being turned away before IIS.
  5. The three donuts. Status codes, methods and HTTP.sys reasons. A new code or reason in the top five is often the first sign of a change.
  6. The two tables. Requests to investigate lists the latest 4xx and 5xx requests with method, status, user, client and server. HTTP API error detail does the same for HTTPERR records, which don't carry a user name.

The table of requests doesn't include the path or the sub-status. Most teams make a copy of the dashboard straight away and add url.original, iis.access.sub_status and, behind a load balancer, network.forwarded_ip as columns. Save the copy under a new name, so reinstalling the pack later doesn't overwrite it.

Read the sub-status and Win32 codes

IIS logs three numbers for every request: the HTTP status (sc-status), an IIS sub-status (sc-substatus) and a Windows error code (sc-win32-status). The pipeline stores them as http.response.status_code, iis.access.sub_status and iis.access.win32_status. The status tells you what the client saw. The other two tell you why, which is what saves you a trip to the server. These are the combinations worth recognising:

Status.sub-statusMeaningUsual cause
401.2Logon failed due to server configurationUsually harmless: the first, anonymous leg of Windows authentication. Only worth a look if it isn't followed by a 200 from the same client
401.1Logon failedWrong credentials. Many from one client is a password guessing pattern
403.14Directory listing deniedNo default document, or a request for a folder rather than a page
404.0Not foundMissing file or route, or scanning
404.3MIME map policyA static file type with no MIME mapping, such as a new font or .webmanifest
404.7 or 404.8Request filtering: file extension or hidden segment deniedProbes for .config, bin or App_Data. Expected on any public site
404.13 or 404.15Request filtering: content too long or query string too longUploads above the limit, or bloated query strings from a client bug
500.19Configuration data is invalidA broken web.config or a missing IIS module, often straight after a deployment
500.30ASP.NET Core app failed to start (in-process)The app threw at start-up. The Application event log has the exception
502.3Bad gateway from a forwarding moduleThe ASP.NET Core module or ARR timed out or couldn't reach its backend
502.5ASP.NET Core process failure (out-of-process), or ARR WebSocket failureFor ASP.NET Core out-of-process hosting, the app process wouldn't start or failed to listen; on ARR, this sub-status is a WebSocket failure
503.2Concurrent request limit exceededThe server's appConcurrentRequestLimit was reached

The Win32 status adds detail that HTTP never would. 0 is success. 2 is file not found and 5 is access denied, usually a file permission problem behind a 401.3 or 500. 64 and 1236 mean the connection went away before IIS finished sending, so a large time-taken on those lines is a slow or departed client, not a slow server. Filter them out before you look at response times.

The pack's status donut shows only the HTTP code, so this query gives you the full breakdown of the last 24 hours of errors. Save it as iis-codes.json and send it the same way as the detection query:

{
  "size": 0,
  "query": { "bool": { "filter": [
    { "range": { "@timestamp": { "gte": "now-24h" } } },
    { "term":  { "event.dataset": "iis.access" } },
    { "range": { "http.response.status_code": { "gte": 400 } } }
  ] } },
  "aggs": {
    "status": {
      "terms": { "field": "http.response.status_code", "size": 10 },
      "aggs": {
        "sub_status": { "terms": { "field": "iis.access.sub_status", "size": 5 } },
        "win32":      { "terms": { "field": "iis.access.win32_status", "size": 5 } },
        "top_paths":  { "terms": { "field": "url.original", "size": 5 } }
      }
    }
  }
}

In Discover, the same idea is a Lucene query. This finds configuration errors on any server, newest first:

event.dataset:iis.access AND http.response.status_code:500 AND iis.access.sub_status:19

And for response times, the access pipeline converts time-taken from milliseconds to nanoseconds in event.duration, so 1 second is 1000000000. This ranks paths by their 95th percentile over the last hour, leaving out aborted connections:

{
  "size": 0,
  "query": { "bool": {
    "filter": [
      { "range": { "@timestamp": { "gte": "now-1h" } } },
      { "term":  { "event.dataset": "iis.access" } }
    ],
    "must_not": [ { "terms": { "iis.access.win32_status": [64, 1236] } } ]
  } },
  "aggs": {
    "paths": {
      "terms": { "field": "url.original", "size": 10, "order": { "latency.95": "desc" } },
      "aggs": {
        "latency": { "percentiles": { "field": "event.duration", "percents": [50, 95] } }
      }
    }
  }
}

The latency.95 order sorts the paths by their own 95th percentile, slowest first. Divide the values by 1,000,000 for milliseconds. time-taken runs until the last byte is sent, so for large downloads it includes the client's network.

Read the HTTP.sys reasons in HTTPERR

Every HTTPERR line has a reason phrase, stored as iis.error.reason_phrase, and for queued requests an application pool name in iis.error.queue_name. The HTTP API errors by reason donut shows the top five. These are the ones you'll actually see:

ReasonWhat happenedWorry?
Timer_ConnectionIdleA kept-alive connection sat idle until HTTP.sys closed itNo. This is usually most of HTTPERR on a busy site
Timer_MinBytesPerSecondThe client was not receiving the response fast enough (default 150 bytes/sec)Rarely. Lots of them from a few addresses can be a slow-request attack
Hostname, BadRequest, Version_N/SMalformed request, invalid Host header or unsupported HTTP versionUsually scanners. Worth a look if one of your own clients appears
Connection_Abandoned_By_AppPoolA worker process quit or crashed with requests in flightYes. Users got errors. Check the Application and System event logs for the crash
AppOffline503: the application was taken offline, usually by rapid-fail protection after repeated crashes. A manual stop is more often logged as DisabledYes. The site is down for everyone in that pool
QueueFull503: the application pool's request queue is fullYes. The app is too slow or too busy to keep up
AppPoolTimer503: the worker process was too busy to take the requestYes, same family as QueueFull
ConnLimit503: the site's connection limit was reachedYes, unless someone set a deliberately low limit

Because the idle and slow timers dominate on most servers, the donut's top five can hide the important reasons. Pin a negative filter on the dashboard copy, or search for the reasons that mean users were turned away:

event.dataset:iis.error AND iis.error.reason_phrase:(AppOffline OR QueueFull OR AppPoolTimer OR ConnLimit OR Connection_Abandoned_By_AppPool)

The iis.error.queue_name column then tells you which application pool to look at, and host.name which server. That's the stopped-pool pattern: HTTP requests falls for one server, HTTPERR rises and the reasons are AppOffline with your pool's name.

Triage path for IIS errors: rising server errors lead to the sub-status and Win32 codes in the access log, while rising HTTP API error entries with falling requests lead to the HTTP.sys reason and application pool in HTTPERR

Alert on 5xx share and turned-away requests

Logs stacks include alerting with ElastAlert 2 rules, which you enable from Alerting & Notifications in your stack settings. The guide to creating alerting rules walks through it. Three rules cover most IIS estates. First, the share of requests that failed, using the percentage match rule type, so a busy morning doesn't page anyone just for being busy:

name: IIS 5xx share above 5%
type: percentage_match
index: "filebeat-*"
buffer_time:
  minutes: 15
filter:
  - query:
      query_string:
        query: "event.module:iis AND event.dataset:iis.access"
match_bucket_filter:
  - range:
      http.response.status_code:
        gte: 500
max_percentage: 5
min_denominator: 200
query_key: host.name
realert:
  minutes: 30
alert:
  - "email"
email:
  - "[email protected]"

That fires per server when more than 5% of the last 15 minutes of requests were 5xx, as long as there were at least 200 requests. Second, any request turned away because an application pool was stopped, crashed or full. A frequency rule with a low threshold suits this, because these should be rare:

name: IIS app pool turning requests away
type: frequency
index: "filebeat-*"
num_events: 5
timeframe:
  minutes: 5
query_key: host.name
filter:
  - query:
      query_string:
        query: "event.dataset:iis.error AND iis.error.reason_phrase:(AppOffline OR QueueFull OR AppPoolTimer OR Connection_Abandoned_By_AppPool)"
alert:
  - "email"
email:
  - "[email protected]"

Third, a flatline rule on the access log per server. It catches a stopped Filebeat service and a moved log folder, as well as a site that has stopped serving:

name: IIS access log has gone quiet
type: flatline
index: "filebeat-*"
threshold: 1
timeframe:
  minutes: 15
query_key: host.name
filter:
  - query:
      query_string:
        query: "event.module:iis AND event.dataset:iis.access"
alert:
  - "email"
email:
  - "[email protected]"

Raise the threshold to suit the traffic, and leave out servers that are quiet overnight or use a longer window for them. Run each rule with Test only before enabling it, to see how often it would have fired over recent data. A frequency rule on iis.access.sub_status:19 is a cheap extra: one 500.19 is enough to know a configuration change went wrong.

When the IIS panels look wrong

  • The pack isn't detected. No document in the last 24 hours of filebeat-* has event.module:iis. Either the filesets are still disabled in iis.yml, Filebeat uses a plain input instead of the module, the logs are in a folder the paths don't cover or the index has been renamed. The detection query above tells you which.
  • Filebeat is running and nothing arrives. Check the #Fields line for sc-bytes. If it's there without the full standard set, every line is being rejected at indexing. Turn on the Logstash dead letter queue and look in dlq-*: the dead_letter_queue_reason will mention network.forwarded_ip. Then change the field selection. New lines index straight away.
  • HTTP requests is higher than the status donut adds up to. Some lines are stored but unparsed. Search event.dataset:iis.access AND error.message:* and look at event.original, then compare that server's #Fields line with the table above. One site with Host or Site Name ticked is the usual cause.
  • The HTTP API error panels are empty. The error fileset is disabled, or the server is genuinely quiet. Run the bad Host header test to prove the path.
  • Every client is the same few addresses. You're logging the load balancer. Add the X-Forwarded-For custom field and use network.forwarded_ip.
  • Times look an hour out. IIS writes UTC in the W3C date and time fields regardless of the server's zone, and the pipeline reads them as UTC. OpenSearch Dashboards then shows them in your browser's zone. The setting called Use local time for file naming and rollover changes only file names, not the times inside.
  • A u_ex file from yesterday is still growing. That's the local time rollover setting at work on a server far from UTC. It's harmless for the dashboards.
  • url.path is empty in Discover. On current stacks the IIS access path is stored in url.original. Search and group on that.
  • Health checks dominate the request counts. Load balancer probes every few seconds can outnumber real users. Pin a negative filter such as NOT url.original:"/health" on your copy, rather than turning logging off for the probe.
  • Last event received is empty. No IIS events in the selected range. On a site with traffic, check Get-Service filebeat and Filebeat's own log in its logs folder for TLS or permission errors.

Limits and trade-offs

  • The parser is fixed. Ingest pipelines on Logit.io Logs stacks are managed, so you can't add a pattern for your own field order. Changing the W3C field selection to one of the supported sets is the fix, and it takes effect on the next line written.
  • Some HTTP.sys detail is missing. HTTPERR records don't carry a user, a duration or a log level, and lines from idle timeouts have no method, path or status. That's what HTTP.sys writes, not something the pipeline drops.
  • No geolocation by default. The pipeline tries to geolocate source.ip, but behind a load balancer that's a private address with no location. network.forwarded_ip isn't geolocated, which is why the pack has no map.
  • Response time is approximate. time-taken includes sending the response, so slow clients and large downloads inflate it. For server-side latency per endpoint, application traces are more precise.
  • Logs only arrive when IIS flushes them. Expect up to about a minute of delay from IIS log buffering on top of shipping, so alert windows shorter than five minutes will be noisy.
  • Access logs contain personal data. Client addresses, Windows user names, query strings and, with all fields, cookies. Check who can view filebeat-* on your stack, keep retention to what you need and don't put secrets in query strings.
  • Other ways of shipping IIS logs won't fill the pack. Logs sent by NXLog, Winlogbeat or the OpenTelemetry Collector reach your stack, but without event.module:iis and the IIS pipelines. If you collect Windows event logs too, the guide to Windows event logs on Logit.io covers that side.

Start Free Trial

Unlock complete visibility with hosted ELK, Grafana, and Prometheus-backed Observability

Start Free Trial

FAQ

Which IIS versions does the pack work with?

Any IIS that writes W3C logs with a supported field set. IIS 8.5 and 10, on Windows Server 2012 R2 to 2025, are the usual case and support the X-Forwarded-For custom field. IIS 7 and 7.5 logs parse with their own default field set, which doesn't include the referrer, but those versions can't log custom fields.

We host many sites on one server. Can I see them separately?

Yes, two ways. With the default fields, each site logs to its own W3SVC<id> folder and the path is in log.file.path, so a filter such as log.file.path:*W3SVC2* picks one site. With all 22 standard fields, iis.access.site_name carries the same W3SVC<id> value and url.domain carries the host header, which is easier to read.

Does it work for Azure App Service or IIS in containers?

Not directly. The pack expects Filebeat's iis module reading log files on a Windows host. App Service and Windows containers write IIS logs in other places and usually ship them another way, so those records won't carry event.module:iis. For IIS on Windows virtual machines in Azure or AWS, the setup in this guide applies unchanged.

We also run Apache or Nginx. Do the packs overlap?

No. Each pack counts only its own module's events, so the numbers never double up. The Apache access and error log dashboards and the Nginx pack follow the same pattern for Linux web servers, and you can install all of them on one stack.

Can I add panels or change the dashboard?

Yes. The installed dashboard is an ordinary OpenSearch Dashboards saved object. Save a copy under a new name before you edit it. Common first changes are a sub-status donut, url.original and network.forwarded_ip columns in the requests table and a negative filter for health checks and idle timeouts.

Can I try it on the free trial?

Yes. The 14-day free trial includes every feature, including the Dashboard Library, alerting and Hosted OpenSearch MCP. Point Filebeat at one IIS server, run the two test requests and install the pack.

Next steps

Check the #Fields line on every web server and settle on either the default fields or all 22 standard fields, plus X-Forwarded-For if you're behind a load balancer. Enable both filesets in the Filebeat iis module, run the 404 and bad Host header tests and find both in Discover. Then install the Microsoft IIS pack, make a copy with the path and sub-status columns and add the 5xx share and app pool alerts. If you're running your own OpenSearch cluster for Windows web logs, a Logit.io Logs stack gives you managed Logstash, OpenSearch and OpenSearch Dashboards with the Dashboard Library built in. For a dedicated cluster, see Hosted OpenSearch and OpenSearch pricing. To ask questions of the same IIS logs from any MCP-compatible AI assistant, such as which application pool turned requests away during last night's deployment, see Hosted OpenSearch MCP.

Get the latest Elastic Stack & logging resources when you subscribe

Want to see this in action?
Start a free trial and connect logs to your alert workflows.