<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kausar Raza</title>
    <description>The latest articles on DEV Community by Kausar Raza (@kausarraza).</description>
    <link>https://dev.to/kausarraza</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4182821%2Fab780158-8181-49a2-8444-8faa64cc463f.webp</url>
      <title>DEV Community: Kausar Raza</title>
      <link>https://dev.to/kausarraza</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kausarraza"/>
    <language>en</language>
    <item>
      <title>Your Next.js Page Is Not Updating: Read the x-nextjs-cache Header Before You Debug Anything</title>
      <dc:creator>Kausar Raza</dc:creator>
      <pubDate>Tue, 13 Oct 2026 17:55:16 +0000</pubDate>
      <link>https://dev.to/kausarraza/your-nextjs-page-is-not-updating-read-the-x-nextjs-cache-header-before-you-debug-anything-1n5a</link>
      <guid>https://dev.to/kausarraza/your-nextjs-page-is-not-updating-read-the-x-nextjs-cache-header-before-you-debug-anything-1n5a</guid>
      <description>&lt;p&gt;You shipped a change. The database has it, the build succeeded, the deploy finished. You load&lt;br&gt;
the page and it shows the old content.&lt;/p&gt;

&lt;p&gt;At this point most people start debugging the thing they just changed. Nine times out of ten&lt;br&gt;
on a Next.js site, nothing is broken. Before you touch anything, send one request and read one&lt;br&gt;
header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null https://example.com/the-page | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; x-nextjs-cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That header separates "my deploy failed" from "the page is simply stale", and those are&lt;br&gt;
completely different problems. I have confused them three times in two days on this site, and&lt;br&gt;
twice I reached a wrong conclusion and had to walk it back.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the values mean
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;x-nextjs-cache&lt;/code&gt; reports how the response you just received was produced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;HIT&lt;/code&gt;&lt;/strong&gt; — served from the cache, and the cached entry is still
considered fresh. What you see is what the cache holds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;MISS&lt;/code&gt;&lt;/strong&gt; — nothing in the cache for this path, so it was rendered
now. The content is current.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;STALE&lt;/code&gt;&lt;/strong&gt; — &lt;strong&gt;this is the one that fools you.&lt;/strong&gt; The
cached entry has passed its revalidation window, so the server handed you the &lt;em&gt;old&lt;/em&gt; body
and kicked off a regeneration in the background. Your next request will almost certainly be
fresh.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read that third one again, because it is the whole article. A &lt;code&gt;STALE&lt;/code&gt; response is&lt;br&gt;
not a failure. It is the cache doing exactly what it was configured to do: never make a visitor&lt;br&gt;
wait for a rebuild. The cost is that the first person to ask for an expired page gets yesterday's&lt;br&gt;
copy.&lt;/p&gt;
&lt;h2&gt;
  
  
  The trap, with the actual numbers
&lt;/h2&gt;

&lt;p&gt;I had just fixed a metadata bug and deployed it. To verify, I crawled every URL in the sitemap&lt;br&gt;
and checked whether the new tag was present. Result: 175 of 177 pages correct, two pages missing&lt;br&gt;
the tag.&lt;/p&gt;

&lt;p&gt;Two pages. That looks exactly like a real bug — some code path the fix missed. I was already&lt;br&gt;
forming a theory about which one.&lt;/p&gt;

&lt;p&gt;Then I requested one of those two pages again and read the headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;x-nextjs-cache: STALE
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page had not missed the fix. It was serving a copy generated &lt;em&gt;before&lt;/em&gt; the deploy,&lt;br&gt;
while regenerating behind the scenes. The very next request returned the correct tag. There was&lt;br&gt;
no bug. There had never been a bug.&lt;/p&gt;

&lt;p&gt;It gets worse, in an interesting way. Earlier the same day I ran two tools over the site back&lt;br&gt;
to back — a crawler, then a content scanner. The crawler reported dead links that the scanner,&lt;br&gt;
running two minutes later, could not find. Both were correct. &lt;strong&gt;The crawler's own requests&lt;br&gt;
were what expired the cache and triggered regeneration&lt;/strong&gt;, so by the time the second tool&lt;br&gt;
ran it was reading freshly generated pages. The first tool measured the old world; the second&lt;br&gt;
measured the world the first tool had created.&lt;/p&gt;

&lt;p&gt;If you take one habit from this: &lt;strong&gt;when a verification result surprises you, request the&lt;br&gt;
page a second time before you believe it.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What actually controls this
&lt;/h2&gt;

&lt;p&gt;For a route segment, the revalidation window is an export:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/blog/[slug]/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;revalidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means: serve from cache for an hour; after the hour, the next request gets the stale copy&lt;br&gt;
and triggers a rebuild. It does &lt;strong&gt;not&lt;/strong&gt; mean "rebuild every hour" — nothing&lt;br&gt;
regenerates if nobody asks. A page nobody visits stays stale indefinitely, which is why a&lt;br&gt;
low-traffic site can show week-old content and a busy one feels instant.&lt;/p&gt;

&lt;p&gt;Two values are worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;export const revalidate = 0&lt;/code&gt; — never serve from the cache; render per request.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;export const dynamic = "force-dynamic"&lt;/code&gt; — the same effect, stated as intent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A sitemap route handler with &lt;code&gt;revalidate = 3600&lt;/code&gt; is a good illustration. Publish a&lt;br&gt;
post and it will not appear in &lt;code&gt;sitemap.xml&lt;/code&gt; immediately. It appears within the hour,&lt;br&gt;
with no deploy and no manual step. I watched this happen yesterday: the post went live, the&lt;br&gt;
sitemap still listed 61 URLs, and an hour later it listed 62. Nothing was broken in between.&lt;/p&gt;
&lt;h2&gt;
  
  
  When a timer is the wrong tool
&lt;/h2&gt;

&lt;p&gt;Time-based revalidation assumes the content changes on its own schedule. It is the wrong tool&lt;br&gt;
when &lt;em&gt;you&lt;/em&gt; change something and need it live now — particularly when the change was made&lt;br&gt;
outside the application.&lt;/p&gt;

&lt;p&gt;That is the case people hit most often and understand least. If you update rows with a SQL&lt;br&gt;
script, or edit content in a CMS that your app does not know about, the app has no idea anything&lt;br&gt;
happened. No amount of redeploying helps, because the deploy is not what is stale — the cache&lt;br&gt;
entry is.&lt;/p&gt;

&lt;p&gt;For that, invalidate explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;revalidatePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;revalidateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// after a write that the app performed&lt;/span&gt;
&lt;span class="nf"&gt;revalidatePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/blog/the-slug&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things catch people here. &lt;code&gt;revalidatePath&lt;/code&gt; only runs on the server — in a route&lt;br&gt;
handler or a server action — so calling it from a client component does nothing. And it marks the&lt;br&gt;
entry stale rather than rebuilding immediately, so the very next request may still return the old&lt;br&gt;
body with &lt;code&gt;x-nextjs-cache: STALE&lt;/code&gt;. That is not a failure either.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a deploy does and does not clear
&lt;/h2&gt;

&lt;p&gt;This depends on your host more than on Next.js, which is why the answers you find online&lt;br&gt;
contradict each other.&lt;/p&gt;

&lt;p&gt;On a platform that builds fresh and discards the old filesystem, a deploy effectively clears&lt;br&gt;
everything. On a self-hosted setup where you extract a build over the previous one, whether the&lt;br&gt;
on-disk cache survives depends on whether you deleted it. If your deploy copies files over the&lt;br&gt;
top without removing &lt;code&gt;.next/cache&lt;/code&gt;, entries can outlive the deploy that was supposed&lt;br&gt;
to replace them.&lt;/p&gt;

&lt;p&gt;Worth separating from a related failure that looks identical: on Windows, a running process&lt;br&gt;
holds locks on files, so an extract over a live app silently skips them and reports success&lt;br&gt;
anyway. That produces a genuinely half-updated app, not a stale cache — and the header will say&lt;br&gt;
&lt;code&gt;HIT&lt;/code&gt; or &lt;code&gt;MISS&lt;/code&gt; rather than &lt;code&gt;STALE&lt;/code&gt;. I covered that one, and the&lt;br&gt;
deploy order that avoids it, in&lt;br&gt;
&lt;a href="https://knowledgemarkg.com/blog/deploy-nextjs-on-iis-plesk-windows" rel="noopener noreferrer"&gt;the Next.js on IIS and Plesk write-up&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three caches, and only one of them is this header
&lt;/h2&gt;

&lt;p&gt;A lot of the contradictory advice online comes from people solving a different layer and&lt;br&gt;
assuming it was the same one. There are usually three between your render and the visitor's&lt;br&gt;
screen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Next.js data and route cache&lt;/strong&gt; — on your server, reported by
&lt;code&gt;x-nextjs-cache&lt;/code&gt;. This is the one &lt;code&gt;revalidate&lt;/code&gt; and
&lt;code&gt;revalidatePath&lt;/code&gt; control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A CDN or reverse proxy&lt;/strong&gt; in front of it, obeying &lt;code&gt;cache-control&lt;/code&gt;.
It can keep serving an old body long after Next.js has regenerated, and
&lt;code&gt;x-nextjs-cache&lt;/code&gt; will never tell you, because the response may not have reached
Next.js at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The browser.&lt;/strong&gt; Cheap to rule out, so rule it out first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two more headers help you tell them apart. &lt;code&gt;age&lt;/code&gt; is how many seconds an&lt;br&gt;
intermediate cache has been holding the response — a large and growing &lt;code&gt;age&lt;/code&gt; on a page&lt;br&gt;
you keep requesting means something in front of your server is answering. And&lt;br&gt;
&lt;code&gt;x-nextjs-prerender&lt;/code&gt; tells you the page was prerendered rather than rendered per&lt;br&gt;
request, which is useful when you expected a route to be dynamic and it is not.&lt;/p&gt;

&lt;p&gt;The practical ordering: hard-refresh once to eliminate the browser, then compare&lt;br&gt;
&lt;code&gt;curl&lt;/code&gt; against what the browser shows. If curl is correct and the browser is not, it&lt;br&gt;
is local. If both are wrong but &lt;code&gt;x-nextjs-cache&lt;/code&gt; says &lt;code&gt;MISS&lt;/code&gt;, you are&lt;br&gt;
probably looking at something in front of Next.js, not Next.js itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading it without curl
&lt;/h2&gt;

&lt;p&gt;In Chrome or Firefox DevTools, open the Network tab, tick &lt;em&gt;Disable cache&lt;/em&gt;, reload, click&lt;br&gt;
the document request — the very first one, not an asset — and read Response Headers. The value is&lt;br&gt;
there on every request Next.js serves, so you can watch it flip from &lt;code&gt;STALE&lt;/code&gt; to&lt;br&gt;
&lt;code&gt;HIT&lt;/code&gt; on a second reload without leaving the browser.&lt;/p&gt;

&lt;p&gt;One caveat that wastes time: if you are looking at a client-side navigation rather than a full&lt;br&gt;
page load, there is no document request to inspect. Reload the URL directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist for "is my change actually live?"
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Request the page twice.&lt;/strong&gt; If the second response differs from the first, you
were looking at a stale entry, not a bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read &lt;code&gt;x-nextjs-cache&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;STALE&lt;/code&gt; means wait and retry;
&lt;code&gt;HIT&lt;/code&gt; or &lt;code&gt;MISS&lt;/code&gt; with wrong content means something real is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the content at the source.&lt;/strong&gt; Hit your API or query the database directly.
If the source is wrong, the cache is innocent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide which layer is stale.&lt;/strong&gt; Content changed outside the app needs
&lt;code&gt;revalidatePath&lt;/code&gt;, not a redeploy. Code changes need a deploy, and a timer will never
fix them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Only then start debugging.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compiled things — components, exported metadata, redirects in&lt;br&gt;
&lt;code&gt;next.config&lt;/code&gt;, anything with a &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; prefix — always need a&lt;br&gt;
rebuild, because they are baked in at build time. That last one catches people constantly; I&lt;br&gt;
wrote about a related environment-variable trap in&lt;br&gt;
&lt;a href="https://knowledgemarkg.com/blog/nextjs-env-local-overrides-env-production" rel="noopener noreferrer"&gt;how .env.local silently overrides&lt;br&gt;
.env.production&lt;/a&gt;. If you are newer to the framework, the&lt;br&gt;
&lt;a href="https://knowledgemarkg.com/blog/nextjs-15-tutorial-complete-guide-from-beginner-to-advanced-2025" rel="noopener noreferrer"&gt;complete Next.js&lt;br&gt;
tutorial&lt;/a&gt; covers the rendering model this builds on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;A page showing old content is usually not a broken deploy. It is a cache entry doing its job,&lt;br&gt;
and one header tells you which. Checking it costs one request. Not checking it cost me two wrong&lt;br&gt;
conclusions in two days, and both times I was already three steps into debugging code that was&lt;br&gt;
working perfectly.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What does x-nextjs-cache: STALE mean?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The cached copy has passed its revalidation window, so Next.js served you the old body and started regenerating the page in the background. It is not an error. Request the page again and you will almost certainly get the fresh version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between HIT, MISS and STALE?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;HIT means it came from the cache and is still considered fresh. MISS means there was nothing cached so it was rendered now, which makes it current. STALE means the entry expired, you received the old body, and a rebuild was triggered behind your request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is my Next.js page not updating after deploy?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Request it twice and read x-nextjs-cache. If the first response is STALE and the second is correct, nothing was wrong. If it says HIT or MISS and the content is still wrong, then check the source data and whether the deploy actually replaced the files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does export const revalidate = 3600 rebuild the page every hour?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. It means the cached copy is considered fresh for an hour. Nothing regenerates unless someone requests the page after that window, so a page nobody visits can stay stale indefinitely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is revalidatePath not working?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two usual reasons. It only runs server-side, so calling it from a client component does nothing. And it marks the entry stale rather than rebuilding immediately, so the next request can still return the old body with a STALE header before the fresh one appears.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I changed data directly in the database and the page still shows the old content.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The application never saw the change, so nothing invalidated the cache, and redeploying will not help because the deploy is not what is stale. Either call revalidatePath for the affected routes, or wait out the revalidate window on that route.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does deploying clear the Next.js cache?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It depends on the host. A platform that builds fresh and discards the old filesystem effectively clears it. On a self-hosted setup where you extract a build over the previous one, cache entries can survive unless you delete .next/cache as part of the deploy.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://knowledgemarkg.com/blog/nextjs-page-not-updating-x-nextjs-cache" rel="noopener noreferrer"&gt;knowledgemarkg.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>caching</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Deploying ASP.NET Core to Plesk on Windows: Every Error I Hit and What It Actually Meant</title>
      <dc:creator>Kausar Raza</dc:creator>
      <pubDate>Tue, 13 Oct 2026 17:51:32 +0000</pubDate>
      <link>https://dev.to/kausarraza/deploying-aspnet-core-to-plesk-on-windows-every-error-i-hit-and-what-it-actually-meant-27l3</link>
      <guid>https://dev.to/kausarraza/deploying-aspnet-core-to-plesk-on-windows-every-error-i-hit-and-what-it-actually-meant-27l3</guid>
      <description>&lt;p&gt;Every guide to deploying ASP.NET Core on Plesk says the same four things: install the hosting&lt;br&gt;
component, run &lt;code&gt;dotnet publish&lt;/code&gt;, upload the folder, set the application pool. That is&lt;br&gt;
all correct, and I have never had a deploy where those four steps were the hard part.&lt;/p&gt;

&lt;p&gt;The hard part is everything that happens after, where the server answers with a three-digit&lt;br&gt;
sub-code and no explanation. This is that list, in the order you actually meet it. This site's&lt;br&gt;
API runs on Plesk shared Windows hosting, so all of it is first-hand.&lt;/p&gt;
&lt;h2&gt;
  
  
  Publish the right thing first
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet publish src/YourApi/YourApi.csproj &lt;span class="nt"&gt;-c&lt;/span&gt; Release &lt;span class="nt"&gt;-o&lt;/span&gt; ./publish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Upload the &lt;em&gt;contents&lt;/em&gt; of &lt;code&gt;publish&lt;/code&gt;, not the folder itself. Framework-dependent&lt;br&gt;
is fine and much smaller, as long as the server has the matching runtime — which is the first&lt;br&gt;
thing that goes wrong.&lt;/p&gt;
&lt;h2&gt;
  
  
  500.31 — the runtime is not there
&lt;/h2&gt;

&lt;p&gt;Full text: &lt;em&gt;Failed to load ASP.NET Core runtime&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The server does not have the &lt;strong&gt;ASP.NET Core Hosting Bundle&lt;/strong&gt; for your target&lt;br&gt;
framework, or has an older one. Note it is the &lt;em&gt;Hosting Bundle&lt;/em&gt; specifically, not the SDK&lt;br&gt;
and not the plain runtime: the bundle is what installs the ASP.NET Core Module into IIS.&lt;/p&gt;

&lt;p&gt;On shared hosting you cannot install it yourself. Two options: ask support which versions are&lt;br&gt;
installed and target one of those, or publish self-contained so the runtime travels with the app.&lt;br&gt;
Self-contained turns a 6 MB upload into about 70 MB, which is a fair trade for not filing a&lt;br&gt;
support ticket every time .NET ships a major version.&lt;/p&gt;
&lt;h2&gt;
  
  
  500.30 — the app started and then died
&lt;/h2&gt;

&lt;p&gt;Full text: &lt;em&gt;ASP.NET Core app failed to start&lt;/em&gt;. The module launched your process and the&lt;br&gt;
process exited. Three causes cover almost all of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The application pool is wrong.&lt;/strong&gt; It must be set to &lt;strong&gt;No Managed Code&lt;/strong&gt;.&lt;br&gt;
ASP.NET Core does not run on the .NET CLR that the pool loads, and a pool set to "v4.0" will fail&lt;br&gt;
in a way that looks like your code crashed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuration is missing at runtime.&lt;/strong&gt; Anything the app reads during startup —&lt;br&gt;
a connection string, an API key — that is present on your machine and absent on the server will&lt;br&gt;
throw before the first request is served.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Something in &lt;code&gt;Program.cs&lt;/code&gt; throws.&lt;/strong&gt; Which you cannot see, until you&lt;br&gt;
turn the log on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;aspNetCore&lt;/span&gt; &lt;span class="na"&gt;processPath=&lt;/span&gt;&lt;span class="s"&gt;"dotnet"&lt;/span&gt; &lt;span class="na"&gt;arguments=&lt;/span&gt;&lt;span class="s"&gt;".\YourApi.dll"&lt;/span&gt;
            &lt;span class="na"&gt;stdoutLogEnabled=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="na"&gt;stdoutLogFile=&lt;/span&gt;&lt;span class="s"&gt;".\logs\stdout"&lt;/span&gt;
            &lt;span class="na"&gt;hostingModel=&lt;/span&gt;&lt;span class="s"&gt;"inprocess"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the &lt;code&gt;logs&lt;/code&gt; folder yourself — the module will not create it, and if it cannot&lt;br&gt;
write there you get the same blank 500.30 with no log. Read the stack trace, fix it, then set&lt;br&gt;
&lt;code&gt;stdoutLogEnabled&lt;/code&gt; back to &lt;code&gt;false&lt;/code&gt;. It is not a logger; it grows forever.&lt;/p&gt;
&lt;h2&gt;
  
  
  500.19 — IIS could not read your web.config
&lt;/h2&gt;

&lt;p&gt;This is the one that fools people, because it has nothing to do with your application. IIS&lt;br&gt;
failed to &lt;em&gt;parse the configuration file&lt;/em&gt;, so the app never started at all.&lt;/p&gt;

&lt;p&gt;On shared hosting the usual cause is a &lt;strong&gt;locked section&lt;/strong&gt;. Hosts lock parts of&lt;br&gt;
&lt;code&gt;system.webServer&lt;/code&gt; at server level, and writing a locked section into your site's&lt;br&gt;
&lt;code&gt;web.config&lt;/code&gt; makes IIS reject &lt;strong&gt;the entire file&lt;/strong&gt; — not just that section.&lt;/p&gt;

&lt;p&gt;The giveaway: static files fail too. If a plain text file in the same folder also returns 500,&lt;br&gt;
the problem is configuration, not code.&lt;/p&gt;

&lt;p&gt;Keep the file to the minimum ASP.NET Core actually needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="utf-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;location&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"."&lt;/span&gt; &lt;span class="na"&gt;inheritInChildApplications=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;system.webServer&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;handlers&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;add&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"aspNetCore"&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"*"&lt;/span&gt; &lt;span class="na"&gt;verb=&lt;/span&gt;&lt;span class="s"&gt;"*"&lt;/span&gt;
             &lt;span class="na"&gt;modules=&lt;/span&gt;&lt;span class="s"&gt;"AspNetCoreModuleV2"&lt;/span&gt; &lt;span class="na"&gt;resourceType=&lt;/span&gt;&lt;span class="s"&gt;"Unspecified"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/handlers&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;aspNetCore&lt;/span&gt; &lt;span class="na"&gt;processPath=&lt;/span&gt;&lt;span class="s"&gt;"dotnet"&lt;/span&gt; &lt;span class="na"&gt;arguments=&lt;/span&gt;&lt;span class="s"&gt;".\YourApi.dll"&lt;/span&gt;
                  &lt;span class="na"&gt;stdoutLogEnabled=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt; &lt;span class="na"&gt;hostingModel=&lt;/span&gt;&lt;span class="s"&gt;"inprocess"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/system.webServer&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/location&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One piece of folklore worth correcting: not every section people assume is locked actually is.&lt;br&gt;
On this host &lt;code&gt;&amp;lt;httpErrors&amp;gt;&lt;/code&gt; turned out to be allowed, and I had avoided it for&lt;br&gt;
months on the strength of a warning I had written myself. Test a section before you rule it out —&lt;br&gt;
but test by loading a page you know works, because a locked section breaks everything at once.&lt;/p&gt;
&lt;h2&gt;
  
  
  app_offline.htm "not working", and the deploy order that fixes it
&lt;/h2&gt;

&lt;p&gt;Drop a file called &lt;code&gt;app_offline.htm&lt;/code&gt; in the application root and the ASP.NET Core&lt;br&gt;
Module shuts the app down and serves that file to every request. Remove it and the app restarts.&lt;br&gt;
It is the cleanest maintenance switch IIS has.&lt;/p&gt;

&lt;p&gt;When people say it does not work, it is almost always one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wrong name or place.&lt;/strong&gt; It must be exactly &lt;code&gt;app_offline.htm&lt;/code&gt; — not
&lt;code&gt;.html&lt;/code&gt;, not &lt;code&gt;App_Offline.htm&lt;/code&gt; in a subfolder — and it must sit next to
&lt;code&gt;web.config&lt;/code&gt; in the application root, not the site root.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows hid the extension from you.&lt;/strong&gt; You created
&lt;code&gt;app_offline.htm.txt&lt;/code&gt; and Explorer is showing you &lt;code&gt;app_offline.htm&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-flight requests.&lt;/strong&gt; Only new requests get the offline page; existing ones
finish normally.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reason it matters on Windows is the thing it protects you from. &lt;strong&gt;Windows locks&lt;br&gt;
files that a running process has open.&lt;/strong&gt; Extract a new build over a running app and the&lt;br&gt;
extract skips the locked DLLs — and Plesk's extract still reports success. You get a half-updated&lt;br&gt;
application, a version mismatch, and no error anywhere.&lt;/p&gt;

&lt;p&gt;So the order is not optional:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Upload &lt;code&gt;app_offline.htm&lt;/code&gt; to the application root.&lt;/li&gt;
&lt;li&gt;Delete the old files.&lt;/li&gt;
&lt;li&gt;Extract the new build.&lt;/li&gt;
&lt;li&gt;Delete &lt;code&gt;app_offline.htm&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  appsettings.Production.json does not replace appsettings.json
&lt;/h2&gt;

&lt;p&gt;It layers on top of it. Keys present in both are overridden by the environment file; keys that&lt;br&gt;
exist &lt;strong&gt;only&lt;/strong&gt; in the base file still apply.&lt;/p&gt;

&lt;p&gt;That cuts both ways and both have bitten me. A key you thought you had overridden is still&lt;br&gt;
being read from the base file because you spelled the section differently. And a key you assumed&lt;br&gt;
was dead because it is not in the production file is very much alive.&lt;/p&gt;

&lt;p&gt;If a setting is not behaving, log the resolved value at startup once rather than reasoning&lt;br&gt;
about which file won:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Using provider {Provider}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Email:Provider"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"(null)"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And keep &lt;code&gt;appsettings.Production.json&lt;/code&gt; out of source control — it holds the real&lt;br&gt;
connection string. Copy it forward from the previous deploy rather than regenerating it, or you&lt;br&gt;
will overwrite live credentials with&lt;br&gt;
&lt;code&gt;Server=localhost;Database=dev;User Id=sa;Password=changeme;&lt;/code&gt; and spend an hour on a&lt;br&gt;
500.30 that is really a login failure.&lt;/p&gt;
&lt;h2&gt;
  
  
  __EFMigrationsHistory drift
&lt;/h2&gt;

&lt;p&gt;EF Core records applied migrations in a table called &lt;code&gt;__EFMigrationsHistory&lt;/code&gt;, keyed&lt;br&gt;
on the migration's ID — the timestamp in its filename. That ID is the only thing EF compares.&lt;/p&gt;

&lt;p&gt;Here is how it goes wrong. A migration is applied to production. Later, someone regenerates&lt;br&gt;
that migration — a rename, a rebase, a merge that recreated the file — and it comes back with a&lt;br&gt;
&lt;strong&gt;new timestamp&lt;/strong&gt;. EF now sees an ID it has no record of, concludes the migration&lt;br&gt;
has never run, and tries to apply it again. On a table that already exists, that fails.&lt;/p&gt;

&lt;p&gt;The fix is to tell the history table what it already knows, idempotently, so re-running it is&lt;br&gt;
harmless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;__EFMigrationsHistory&lt;/span&gt;
               &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;MigrationId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'20260927134609_AddSomeColumn'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;__EFMigrationsHistory&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MigrationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ProductVersion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'20260927134609_AddSomeColumn'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'9.0.0'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not delete rows from this table to "force a rerun" unless you have checked that the&lt;br&gt;
migration is genuinely reversible against live data. It is a ledger, not a cache.&lt;/p&gt;
&lt;h2&gt;
  
  
  Give yourself a way to tell which build is live
&lt;/h2&gt;

&lt;p&gt;Because a Windows deploy can half-succeed, "did my change actually ship?" is a question you&lt;br&gt;
will ask often. Guessing from behaviour is slow. A tiny unauthenticated endpoint answers it in a&lt;br&gt;
second:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/v1/version"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Program&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Assembly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetName&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;built&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetLastWriteTimeUtc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Program&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Assembly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"u"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One warning from my own code, which I am in the middle of fixing. When I first added this I&lt;br&gt;
also returned the assembly's full path, because it was useful while debugging a deploy that had&lt;br&gt;
half-extracted. That ships the absolute directory layout of the server to anyone who asks, and&lt;br&gt;
it stayed in production far longer than the debugging session did.&lt;/p&gt;

&lt;p&gt;Return a version and a timestamp. Do not return paths, environment variables, connection&lt;br&gt;
state, or the machine name. If you need those while chasing a problem, add them for the hour and&lt;br&gt;
take them out again — a diagnostic endpoint is the easiest thing in a codebase to forget about,&lt;br&gt;
because it never breaks and nothing tests it.&lt;/p&gt;

&lt;h2&gt;
  
  
  One last thing: the database is probably not local
&lt;/h2&gt;

&lt;p&gt;On shared hosting your SQL Server usually lives on a different box, sometimes a different&lt;br&gt;
datacentre. Every query crosses a network, and the first one after an idle period pays a cold&lt;br&gt;
start. Local development against that database feels broken when it is merely remote.&lt;/p&gt;

&lt;p&gt;This is worth knowing before you spend an afternoon profiling EF Core over a 40 ms round trip.&lt;br&gt;
Measure against the server, not your laptop, and judge a query by its round-trip count before its&lt;br&gt;
execution plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is Plesk the right place for this?
&lt;/h2&gt;

&lt;p&gt;If the API is the only thing you are hosting, a small Linux VPS running Kestrel behind nginx&lt;br&gt;
is simpler and better documented. Plesk on Windows earns its place when something else already&lt;br&gt;
requires that box — a SQL Server licence, a legacy ASP.NET app, or a client who buys Windows&lt;br&gt;
hosting and will not move.&lt;/p&gt;

&lt;p&gt;That is the situation here, and it is stable once the five problems above are behind you. If&lt;br&gt;
you are running the front end on the same stack, I wrote up&lt;br&gt;
&lt;a href="https://knowledgemarkg.com/blog/deploy-nextjs-on-iis-plesk-windows" rel="noopener noreferrer"&gt;the matching set of traps for Next.js on IIS&lt;br&gt;
and Plesk&lt;/a&gt; — different runtime, same hosting model, and a couple of the failures are&lt;br&gt;
identical. And if you are still choosing, this is the&lt;br&gt;
&lt;a href="https://knowledgemarkg.com/blog/windows-vps-vs-linux-vps-aspnet-core" rel="noopener noreferrer"&gt;Windows VPS versus Linux VPS comparison&lt;/a&gt;&lt;br&gt;
I would read first.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What does ASP.NET Core error 500.31 mean?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The ASP.NET Core Hosting Bundle for your target framework is not installed on the server, or is an older version. It must be the Hosting Bundle, not the SDK or the plain runtime, because the bundle is what installs the ASP.NET Core Module into IIS. On shared hosting, either target a version the host already has or publish self-contained.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I fix ASP.NET Core 500.30, app failed to start?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The process launched and exited. Check that the application pool is set to No Managed Code, that every setting the app reads at startup exists on the server, and then turn on stdoutLogEnabled in web.config to read the actual exception. Create the logs folder yourself - the module will not, and without it you get the same blank error with no log.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do I get HTTP Error 500.19 on IIS?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;IIS could not parse web.config, so your app never started. On shared hosting the usual cause is a system.webServer section the host locks at server level; writing one makes IIS reject the whole file rather than that section. The giveaway is that static files in the same folder fail too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is app_offline.htm not working?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Usually the name or the location. It must be exactly app_offline.htm, in the application root next to web.config, not the site root - and check Windows is not hiding a .txt extension from you. Requests already in flight also finish normally; only new ones get the offline page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does appsettings.Production.json replace appsettings.json?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No, it layers on top. Keys in both are overridden by the environment file, but keys that exist only in the base file still apply. If a setting is not behaving, log the resolved value at startup rather than reasoning about which file won.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is EF Core trying to apply a migration that already ran?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;EF compares the migration ID, which is the timestamp in the filename. If the migration file was regenerated after it was applied, it comes back with a new timestamp, so EF sees an ID it has no record of and runs it again. Insert the new ID into __EFMigrationsHistory idempotently rather than deleting rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you deploy ASP.NET Core to Plesk shared hosting at all?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, and it is stable once configured. You cannot install the Hosting Bundle or edit applicationHost.config yourself, so you work within what the host allows. It makes most sense when something else already requires the Windows box, such as a SQL Server licence or a legacy ASP.NET application.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://knowledgemarkg.com/blog/deploy-aspnet-core-plesk-500-errors" rel="noopener noreferrer"&gt;knowledgemarkg.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>aspnetcore</category>
      <category>iis</category>
      <category>devops</category>
    </item>
    <item>
      <title>Deploying Next.js on IIS and Plesk: The Windows Traps Nobody Documents</title>
      <dc:creator>Kausar Raza</dc:creator>
      <pubDate>Tue, 13 Oct 2026 17:49:32 +0000</pubDate>
      <link>https://dev.to/kausarraza/deploying-nextjs-on-iis-and-plesk-the-windows-traps-nobody-documents-21ke</link>
      <guid>https://dev.to/kausarraza/deploying-nextjs-on-iis-and-plesk-the-windows-traps-nobody-documents-21ke</guid>
      <description>&lt;p&gt;Search for how to deploy Next.js on IIS and you will find the same answer repeated: install&lt;br&gt;
Application Request Routing, run the app under PM2, and point an IIS reverse proxy rule at&lt;br&gt;
&lt;code&gt;localhost:3000&lt;/code&gt;. That is good advice if you own the server. On Plesk shared Windows&lt;br&gt;
hosting you cannot install ARR, you cannot run PM2 as a service, and you cannot edit&lt;br&gt;
&lt;code&gt;applicationHost.config&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This site runs on that stack. What follows is the configuration that actually works, plus the&lt;br&gt;
four failures that cost me real hours - each one silent, and each one looking like a different&lt;br&gt;
problem than it was.&lt;/p&gt;
&lt;h2&gt;
  
  
  What you are actually working with
&lt;/h2&gt;

&lt;p&gt;Plesk's Windows Node.js support runs your app through &lt;strong&gt;iisnode&lt;/strong&gt;, an IIS module&lt;br&gt;
that hosts a Node process inside IIS rather than beside it. There is no second port, no PM2, no&lt;br&gt;
reverse proxy. IIS owns the request, hands it to Node, and takes the response back.&lt;/p&gt;

&lt;p&gt;That last part matters more than it sounds. It is where the 404 problem further down comes&lt;br&gt;
from.&lt;/p&gt;
&lt;h2&gt;
  
  
  Build for standalone output
&lt;/h2&gt;

&lt;p&gt;Set the output mode in &lt;code&gt;next.config.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;standalone&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A standalone build produces a self-contained &lt;code&gt;server.js&lt;/code&gt; plus a pruned&lt;br&gt;
&lt;code&gt;node_modules&lt;/code&gt; holding only what the app actually imports. On shared hosting, where&lt;br&gt;
you are not running &lt;code&gt;npm install&lt;/code&gt; on the server, that is the difference between a&lt;br&gt;
25 MB upload and a 400 MB one.&lt;/p&gt;

&lt;p&gt;The trap is that &lt;code&gt;next build&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; put everything you need&lt;br&gt;
into &lt;code&gt;.next/standalone&lt;/code&gt;. Two directories are left out by design, and the app boots&lt;br&gt;
perfectly without them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.next/static&lt;/code&gt; → copy to &lt;code&gt;.next/static&lt;/code&gt; in the deploy folder&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;public&lt;/code&gt; → copy to &lt;code&gt;public&lt;/code&gt; in the deploy folder&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Miss either one and you get a 200 response, correct HTML, and a completely unstyled page with&lt;br&gt;
no JavaScript. Nothing errors, nothing logs.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the deploy folder has to look like
&lt;/h2&gt;

&lt;p&gt;Because you are assembling the folder by hand rather than letting a platform do it, it helps&lt;br&gt;
to know exactly what the finished thing contains. Mine is produced by a script, and the layout&lt;br&gt;
is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deploy-folder/
  server.js            &amp;lt;- from .next/standalone
  app.js               &amp;lt;- the iisnode shim, written by hand
  web.config           &amp;lt;- written by hand
  package.json         &amp;lt;- written by hand, not the project one
  node_modules/        &amp;lt;- from .next/standalone, already pruned
  .next/
    BUILD_ID
    server/            &amp;lt;- from .next/standalone
    static/            &amp;lt;- copied separately, see above
  public/              &amp;lt;- copied separately, see above
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two of those are worth a comment. The &lt;code&gt;package.json&lt;/code&gt; at the root is not your&lt;br&gt;
project's - it is a minimal file telling the host what to start. And if you script this copy on&lt;br&gt;
Windows, force long-path support: &lt;code&gt;node_modules&lt;/code&gt; nests well past the 260-character&lt;br&gt;
limit, and without the extended-length prefix the copy fails silently part-way through, leaving&lt;br&gt;
you with a folder that looks complete and boots to an error.&lt;/p&gt;

&lt;p&gt;Finally, anything in &lt;code&gt;public/&lt;/code&gt; that proves ownership of the domain - a Search&lt;br&gt;
Console verification file, an IndexNow key file - has to survive every deploy. Both fail&lt;br&gt;
silently and slowly if they go missing: Google quietly unverifies the property, and IndexNow&lt;br&gt;
rejects every submission. Add an assertion to your staging script so a missing verification file&lt;br&gt;
fails the build rather than the search engine.&lt;/p&gt;
&lt;h2&gt;
  
  
  The web.config that works
&lt;/h2&gt;

&lt;p&gt;Keep it minimal. Every section you add is a section that might be locked at server level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="utf-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;system.webServer&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;handlers&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;add&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"iisnode"&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"app.js"&lt;/span&gt; &lt;span class="na"&gt;verb=&lt;/span&gt;&lt;span class="s"&gt;"*"&lt;/span&gt; &lt;span class="na"&gt;modules=&lt;/span&gt;&lt;span class="s"&gt;"iisnode"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/handlers&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;rewrite&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;rules&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;rule&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"DynamicContent"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;match&lt;/span&gt; &lt;span class="na"&gt;url=&lt;/span&gt;&lt;span class="s"&gt;"/*"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;action&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"Rewrite"&lt;/span&gt; &lt;span class="na"&gt;url=&lt;/span&gt;&lt;span class="s"&gt;"app.js"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/rule&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/rules&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/rewrite&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;httpErrors&lt;/span&gt; &lt;span class="na"&gt;existingResponse=&lt;/span&gt;&lt;span class="s"&gt;"PassThrough"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;iisnode&lt;/span&gt; &lt;span class="na"&gt;loggingEnabled=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt; &lt;span class="na"&gt;devErrorsEnabled=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/system.webServer&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handler path has to match a real file, so &lt;code&gt;app.js&lt;/code&gt; is a two-line shim that&lt;br&gt;
requires the standalone server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./server.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Trap 1: one locked section takes the whole site down
&lt;/h2&gt;

&lt;p&gt;Shared hosting locks several &lt;code&gt;system.webServer&lt;/code&gt; sections at server level -&lt;br&gt;
&lt;code&gt;&amp;lt;security&amp;gt;&amp;lt;requestFiltering&amp;gt;&lt;/code&gt; is the usual culprit. Writing a locked&lt;br&gt;
section into your site's &lt;code&gt;web.config&lt;/code&gt; does not produce a message about that section.&lt;br&gt;
IIS fails to parse &lt;strong&gt;the entire file&lt;/strong&gt;, and every URL returns an empty HTTP 500,&lt;br&gt;
including static files that never reach Node.&lt;/p&gt;

&lt;p&gt;It reads exactly like an application crash. It is a configuration parse error. The&lt;br&gt;
distinguishing signal is that &lt;code&gt;/favicon.ico&lt;/code&gt; fails too - if Node were the problem,&lt;br&gt;
static files would still serve.&lt;/p&gt;

&lt;p&gt;Keep a known-good copy of &lt;code&gt;web.config&lt;/code&gt; beside the live one so the rollback is a&lt;br&gt;
file copy rather than a debugging session.&lt;/p&gt;

&lt;p&gt;One correction to the folklore: &lt;code&gt;&amp;lt;httpErrors&amp;gt;&lt;/code&gt; is widely &lt;em&gt;assumed&lt;/em&gt; to&lt;br&gt;
be locked and frequently is not. Plesk's own "Custom error documents" feature writes that exact&lt;br&gt;
section into site &lt;code&gt;web.config&lt;/code&gt; files, which it could not do if the section were&lt;br&gt;
locked. Test it rather than avoiding it - and test by loading your &lt;strong&gt;homepage&lt;/strong&gt;,&lt;br&gt;
not a 404, because a locked section breaks everything, not just error pages.&lt;/p&gt;
&lt;h2&gt;
  
  
  Trap 2: IIS throws away your custom 404 page
&lt;/h2&gt;

&lt;p&gt;This one is invisible unless you go looking. Your &lt;code&gt;app/not-found.tsx&lt;/code&gt; renders&lt;br&gt;
correctly, the status code is a correct 404, and visitors still get the grey IIS page reading&lt;br&gt;
&lt;em&gt;404 - File or directory not found.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here is how to prove where it happens. Request a missing URL and read the headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null https://example.com/no-such-page
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the response carries &lt;code&gt;x-nextjs-cache&lt;/code&gt; and &lt;code&gt;x-nextjs-prerender&lt;/code&gt;&lt;br&gt;
headers but the body is about 1 KB of Verdana-styled HTML, then Next rendered your page and IIS&lt;br&gt;
replaced the body afterwards. The headers survive the swap; the body does not. That single&lt;br&gt;
detail is what tells you to stop debugging your Next.js route and go and look at IIS.&lt;/p&gt;

&lt;p&gt;The cause is IIS's &lt;code&gt;existingResponse&lt;/code&gt; behaviour, which defaults to replacing the&lt;br&gt;
body of any response with an error status. One line fixes it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;httpErrors&lt;/span&gt; &lt;span class="na"&gt;existingResponse=&lt;/span&gt;&lt;span class="s"&gt;"PassThrough"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth knowing. First, this is presentation, not SEO: the status code was a correct&lt;br&gt;
404 the whole time, so search engines were never misled and nothing was indexed that should not&lt;br&gt;
have been. Second, once IIS stops replacing error bodies it also stops masking &lt;em&gt;500&lt;/em&gt;&lt;br&gt;
bodies - which is exactly why &lt;code&gt;devErrorsEnabled&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt; in the config&lt;br&gt;
above.&lt;/p&gt;
&lt;h2&gt;
  
  
  Trap 3: environment variables are not what you think
&lt;/h2&gt;

&lt;p&gt;Two rules catch people on every Next.js host, and they bite harder on shared hosting where&lt;br&gt;
the control panel offers you an environment-variable editor that looks authoritative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;NEXT_PUBLIC_*&lt;/code&gt; is inlined at build time.&lt;/strong&gt; Anything with that&lt;br&gt;
prefix is compiled into the JavaScript bundle during &lt;code&gt;next build&lt;/code&gt;. Setting it in&lt;br&gt;
Plesk's Node.js panel afterwards does nothing whatsoever, because the string is already baked in.&lt;br&gt;
If it has to change, you rebuild.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.env.local&lt;/code&gt; loads in production builds too, and it wins.&lt;/strong&gt; It is&lt;br&gt;
not a development-only file, and it overrides &lt;code&gt;.env.production&lt;/code&gt;. I wrote about&lt;br&gt;
&lt;a href="https://knowledgemarkg.com/blog/nextjs-env-local-overrides-env-production" rel="noopener noreferrer"&gt;how .env.local silently overrides&lt;br&gt;
.env.production&lt;/a&gt; separately, because it once baked a localhost API URL into a live build. The&lt;br&gt;
safest habit is to name the file &lt;code&gt;.env.development.local&lt;/code&gt;, which cannot load in a&lt;br&gt;
production build by accident.&lt;/p&gt;
&lt;h2&gt;
  
  
  Trap 4: deploy order, because Windows locks files
&lt;/h2&gt;

&lt;p&gt;Windows holds a lock on files the running process has open. Extract a new build over a running&lt;br&gt;
app and the extract silently skips the locked files - while still reporting success. You end up&lt;br&gt;
with a half-updated application and no error anywhere.&lt;/p&gt;

&lt;p&gt;The order that works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Put an &lt;code&gt;app_offline.htm&lt;/code&gt; in the application root. IIS stops the app and serves
that file for every request.&lt;/li&gt;
&lt;li&gt;Delete the old files.&lt;/li&gt;
&lt;li&gt;Extract the new build.&lt;/li&gt;
&lt;li&gt;Remove &lt;code&gt;app_offline.htm&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before uploading, confirm the archive actually contains the change you made. A stale build&lt;br&gt;
zips just as happily as a fresh one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rl&lt;/span&gt; &lt;span class="s2"&gt;"a string unique to this change"&lt;/span&gt; staging/.next/static/chunks | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What you do and do not have to redeploy for
&lt;/h2&gt;

&lt;p&gt;Because every deploy here is a manual zip-and-extract, it is worth being clear about which&lt;br&gt;
changes actually need one. On this setup, with incremental regeneration doing the work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No redeploy needed&lt;/strong&gt; - new or edited content coming from your API or database,
and anything a route handler builds from it. If your sitemap route has
&lt;code&gt;revalidate = 3600&lt;/code&gt;, a new page appears in it within the hour on its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redeploy needed&lt;/strong&gt; - anything compiled: components, metadata exported from a
page, &lt;code&gt;next.config&lt;/code&gt; redirects, and every &lt;code&gt;NEXT_PUBLIC_*&lt;/code&gt; value.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One consequence worth internalising: a stale cached page can make you think a code fix did not&lt;br&gt;
work. After a deploy I checked a page, found the new metadata missing, and nearly started&lt;br&gt;
debugging - the response header said &lt;code&gt;x-nextjs-cache: STALE&lt;/code&gt;, meaning the old body was&lt;br&gt;
being served while the page regenerated behind it. The next request was correct. Check that&lt;br&gt;
header before you conclude anything about a deploy.&lt;/p&gt;

&lt;p&gt;The other half of that rule is the one people get wrong in the opposite direction: a route&lt;br&gt;
handler can only output a field the API already returns. Shipping the frontend first and the&lt;br&gt;
backend later gives you a sitemap that silently omits everything.&lt;/p&gt;
&lt;h2&gt;
  
  
  Debugging a 500 you cannot see
&lt;/h2&gt;

&lt;p&gt;If the app will not boot you get a 500 with an empty body and nothing to read. Turn iisnode's&lt;br&gt;
diagnostics on temporarily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;iisnode&lt;/span&gt; &lt;span class="na"&gt;loggingEnabled=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="na"&gt;devErrorsEnabled=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That writes stdout and stderr into an &lt;code&gt;iisnode&lt;/code&gt; folder beside the app and puts the&lt;br&gt;
error detail in the response body. Read it, fix the boot failure, then &lt;strong&gt;turn both back&lt;br&gt;
off&lt;/strong&gt; - especially with &lt;code&gt;existingResponse="PassThrough"&lt;/code&gt; enabled, because&lt;br&gt;
together they will show stack traces to the public.&lt;/p&gt;

&lt;p&gt;One more: if Plesk's Node.js panel offers to "Auto-configure" the application, decline. It&lt;br&gt;
generates its own &lt;code&gt;web.config&lt;/code&gt; and overwrites yours, including the handler path and&lt;br&gt;
the rewrite rule the standalone build depends on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is this the right place to host Next.js at all?
&lt;/h2&gt;

&lt;p&gt;Usually not, if you have a free choice. A small Linux VPS running the standalone server behind&lt;br&gt;
nginx is simpler in every way and the tooling assumes it. The reason to do this is that something&lt;br&gt;
else already lives on the Windows box - an ASP.NET Core API, a SQL Server database, or a client&lt;br&gt;
who buys Windows hosting and is not going to move.&lt;/p&gt;

&lt;p&gt;That is the situation this site is in, and it works fine. It is only that nothing warns you&lt;br&gt;
about the four failures above, and every one of them presents as something other than what it is.&lt;br&gt;
If you are still weighing platforms, I compared&lt;br&gt;
&lt;a href="https://knowledgemarkg.com/blog/windows-vps-vs-linux-vps-aspnet-core" rel="noopener noreferrer"&gt;Windows VPS against Linux VPS for ASP.NET&lt;br&gt;
Core&lt;/a&gt; and most of that reasoning carries over. If you are newer to the framework itself, the&lt;br&gt;
&lt;a href="https://knowledgemarkg.com/blog/nextjs-15-tutorial-complete-guide-from-beginner-to-advanced-2025" rel="noopener noreferrer"&gt;complete Next.js&lt;br&gt;
tutorial&lt;/a&gt; is a better starting point than the host.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can you run Next.js on IIS without a reverse proxy?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. iisnode hosts the Node process inside IIS, so there is no second port and no reverse proxy rule. This is what Plesk's Windows Node.js support uses, and it is the only option on shared hosting where you cannot install Application Request Routing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is my Next.js site loading with no CSS after deploying to IIS?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A standalone build does not include .next/static or public. Both have to be copied into the deploy folder yourself. The app boots and returns correct HTML without them, so nothing errors - you just get an unstyled page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does IIS show its own 404 page instead of my Next.js not-found page?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;IIS replaces the body of any error-status response by default. Adding httpErrors existingResponse="PassThrough" to web.config fixes it. You can confirm that is the cause by checking whether the 404 response still carries x-nextjs-cache headers: if it does, Next rendered the page and IIS swapped the body out afterwards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does every URL return an empty 500 after I edited web.config?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A system.webServer section locked at server level makes IIS fail to parse the whole file, not just that section. Static files fail too, which is how you tell it apart from an application crash. Remove the section you added and the site comes straight back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is my NEXT_PUBLIC environment variable empty in production?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;NEXT_PUBLIC_ values are inlined into the JavaScript bundle during next build. Setting them on the server or in Plesk's Node.js panel afterwards has no effect, because the value is already compiled in. Change it and rebuild.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why did my deploy only partly update the site?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Windows locks files the running process has open, and extracting over a running app silently skips them while still reporting success. Drop an app_offline.htm in first, delete the old files, extract, then remove it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Plesk on Windows a reasonable place to host Next.js?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It works, but it is rarely the easiest choice. It makes sense when something else already requires the Windows box, such as an ASP.NET Core API or SQL Server. If the stack is yours to pick, a small Linux VPS running the standalone server is simpler and far better documented.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://knowledgemarkg.com/blog/deploy-nextjs-on-iis-plesk-windows" rel="noopener noreferrer"&gt;knowledgemarkg.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>iis</category>
      <category>dotnet</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
