<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Web Hosting | John Nessime</title>
	<atom:link href="https://john-nessime.com/blog/tag/web-hosting/feed/" rel="self" type="application/rss+xml" />
	<link>https://john-nessime.com/blog/tag/web-hosting/</link>
	<description>Cloud, DevOps, Data &#38; AI — Built, Tested, Explained</description>
	<lastBuildDate>Sun, 20 Sep 2026 16:32:06 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.4</generator>

<image>
	<url>https://john-nessime.com/blog/wp-content/uploads/2026/07/cropped-jn-32x32.png</url>
	<title>Web Hosting | John Nessime</title>
	<link>https://john-nessime.com/blog/tag/web-hosting/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>DirectAdmin PHP-FPM Pools: Why One WordPress Site Takes Down the Others</title>
		<link>https://john-nessime.com/blog/system-administration/directadmin-php-fpm-pools-wordpress-tuning/</link>
		
		<dc:creator><![CDATA[John Nessime]]></dc:creator>
		<pubDate>Sun, 23 Aug 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[System Administration]]></category>
		<category><![CDATA[Web Hosting]]></category>
		<category><![CDATA[Web Performance]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[CustomBuild]]></category>
		<category><![CDATA[DirectAdmin]]></category>
		<category><![CDATA[Nginx]]></category>
		<category><![CDATA[OPcache]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[PHP-FPM]]></category>
		<category><![CDATA[Shared Hosting]]></category>
		<category><![CDATA[Sysadmin]]></category>
		<category><![CDATA[Troubleshooting]]></category>
		<category><![CDATA[VPS]]></category>
		<category><![CDATA[Website Performance]]></category>
		<category><![CDATA[WordPress Hosting]]></category>
		<guid isPermaLink="false">https://john-nessime.com/blog/?p=246</guid>

					<description><![CDATA[<p>DirectAdmin gives each user one PHP-FPM pool, not each site. That single design fact explains most of the 502s on a busy shared box. Here is how to find the pool files, size pm.max_children from real memory numbers, set per-site limits that WordPress cannot override, and make the changes survive the next rewrite_confs.</p>
<p>The post <a href="https://john-nessime.com/blog/system-administration/directadmin-php-fpm-pools-wordpress-tuning/">DirectAdmin PHP-FPM Pools: Why One WordPress Site Takes Down the Others</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">The ticket said &#8220;the shop is throwing 502s again.&#8221; The shop was fine. What was not fine was the half-forgotten news site sitting on the same DirectAdmin account, running a broken cron that fired an uncached admin-ajax call every few seconds. It had eaten every worker in the pool, and WooCommerce was queued behind it, waiting for a process that was never going to free up in time.</p>



<p class="wp-block-paragraph">That is the thing about <strong>DirectAdmin PHP-FPM pools</strong> that catches people out. You think you are tuning a site. You are not. You are tuning a user, and every domain that user owns is sharing the same worker budget, the same queue, and the same memory ceiling.</p>



<p class="wp-block-paragraph">This post covers where DirectAdmin actually keeps the pool config, how to size <code>pm.max_children</code> from numbers your server gives you rather than from a blog post&#8217;s magic value, which per-site PHP limits are worth setting and which are theatre, why OPcache does not behave the way you expect on a multi-tenant box, and how to make all of it survive the next CustomBuild run.</p>



<h2 class="wp-block-heading">The design fact that explains most of your 502s</h2>



<p class="wp-block-paragraph">DirectAdmin generates one FPM pool per user account, per PHP version. The pool is named after the Linux user. It listens on a Unix socket named after the user. It runs as that user. Apache or Nginx hands PHP requests to that socket for every domain and subdomain in the account.</p>



<p class="wp-block-paragraph">So if a reseller client has twelve domains under one username, those twelve sites share a single value of <code>pm.max_children</code>. There is no per-domain pool. This is a long-standing feature request rather than an oversight, and it has a real consequence: <strong>the noisiest site in an account sets the availability of every other site in that account.</strong></p>



<p class="wp-block-paragraph">Once you have internalised that, the fix for a lot of &#8220;mystery 502&#8221; tickets stops being a config tweak and starts being an account layout decision. If a site matters commercially, give it its own DirectAdmin user. That is the cleanest isolation the panel offers without going off-piste, and it costs you nothing but a bit of admin tidiness.</p>



<p class="wp-block-paragraph">The alternative, if you have User jails enabled, is DirectAdmin&#8217;s isolated FPM mode. That runs a separate <code>php-fpm</code> master process per jailed user instead of a shared master with many pools:</p>



<pre class="wp-block-code"><code>da config-set isolated_fpm 1
systemctl restart directadmin
da taskq --run 'action=rewrite&amp;value=httpd'</code></pre>



<p class="wp-block-paragraph">It only applies to users who already have Jailed enabled. The trade-off is memory: one master process per user, plus a separate OPcache region for each, adds up quickly. DirectAdmin mitigates that by letting idle per-user FPM instances exit after a period of inactivity, but on a box with sixty accounts you will feel it. I would reach for this on a server hosting a handful of paying clients, not on a general shared box.</p>



<h2 class="wp-block-heading">Where the pool config actually lives</h2>



<p class="wp-block-paragraph">This trips up almost everyone the first time, because there are two files with nearly the same name and only one of them matters for user sites.</p>



<ul class="wp-block-list">

<li><code>/usr/local/php83/etc/php-fpm.conf</code> is the master config for that PHP version. Editing it by hand is how you learn that CustomBuild overwrites it.</li>


<li><code>/usr/local/directadmin/data/users/USERNAME/php/php-fpm83.conf</code> is the generated per-user pool. This is the file that governs a customer&#8217;s sites.</li>


<li><code>/usr/local/directadmin/data/templates/php-fpm.conf</code> is the template those per-user files are rendered from.</li>

</ul>



<p class="wp-block-paragraph">Substitute your PHP version for <code>83</code> throughout. To see what every account is currently running:</p>



<pre class="wp-block-code"><code>ls /usr/local/directadmin/data/users/*/php/php-fpm*.conf

grep -H 'pm.max_children' /usr/local/directadmin/data/users/*/php/php-fpm*.conf</code></pre>



<p class="wp-block-paragraph">The second command is the one I run first on any server I have just inherited. It gives you the entire worker budget of the box on one screen, and it is usually the moment you discover that forty accounts are all sitting on the stock default.</p>



<p class="wp-block-paragraph">The DirectAdmin template ships with <code>pm = ondemand</code> and a low default child count, driven by a <code>MAX_CHILDREN</code> token. Ondemand means no workers exist until a request arrives, and idle workers are reaped after a timeout. On a shared box with many mostly-idle accounts, that is the right default and I would leave it alone. On a server dedicated to one busy WordPress site, the spawn latency on the first request after an idle period is real and switching to <code>dynamic</code> is defensible. Most people who switch to <code>dynamic</code> on a shared server are trading memory they do not have for a latency win they will not notice.</p>



<h2 class="wp-block-heading">Sizing pm.max_children without guessing</h2>



<p class="wp-block-paragraph">Every tuning guide gives you a formula. The formula is fine. The input everyone gets wrong is worker memory, because they use a number from someone else&#8217;s server instead of measuring their own. A lean WordPress install and the same install with a page builder, a security plugin and WooCommerce are not remotely the same workload.</p>



<p class="wp-block-paragraph">Measure it. This averages the resident memory of the workers currently running in one pool:</p>



<pre class="wp-block-code"><code>ps -eo rss,args | grep '&#91;p]hp-fpm: pool wpuser' 
  | awk '{sum+=$1; n++} END {if (n) printf "%d workers, avg %.0f MBn", n, sum/n/1024}'</code></pre>



<p class="wp-block-paragraph">The bracket trick in the grep pattern stops grep matching its own process. <code>rss</code> is resident set size in kilobytes, which is why the awk divides twice. Run it during a busy period, not at three in the morning, and run it a few times.</p>



<p class="wp-block-paragraph">Two honest caveats about RSS. It overstates real consumption, because workers forked from the same master share a lot of pages, including the OPcache region. And it understates your worst case, because a worker that has not yet hit a memory-hungry request looks cheap. Treat the average as a planning figure, not a guarantee, and leave headroom.</p>



<p class="wp-block-paragraph">Then the arithmetic:</p>



<ol class="wp-block-list">

<li>Take total RAM and subtract what MySQL or MariaDB, the web server, mail, and the panel itself need. On a WordPress box the database is usually the second-largest consumer and it is not optional.</li>


<li>Reserve a further slice for page cache and burst. I aim to leave a comfortable margin free rather than allocating to the last megabyte.</li>


<li>Divide what is left by your measured per-worker figure. That is your ceiling for <em>all</em> pools combined.</li>


<li>Split that ceiling across accounts by actual traffic, not evenly.</li>

</ol>



<p class="wp-block-paragraph">Step three is where people go wrong on cheap VPS plans. If you are running a dozen WordPress sites on one of the low-RAM tiers from a provider like Contabo or InterServer, the honest answer is often that you cannot give every account a generous pool and you need to either cut the site count or move up a tier. Setting <code>pm.max_children</code> to a number your RAM cannot back does not buy capacity. It buys the OOM killer, and the OOM killer usually takes the database with it, which turns a slow site into a broken one.</p>



<p class="wp-block-paragraph">To raise the server-wide default:</p>



<pre class="wp-block-code"><code>da config-set php_fpm_max_children_default 30
systemctl restart directadmin
da build rewrite_confs
systemctl restart php-fpm83</code></pre>



<p class="wp-block-paragraph">For a single account, go to <em>Admin Level &rarr; Custom HTTPD Configurations &rarr; domain.com</em> and open the <code>php-fpm</code> entry for that PHP version. In the <code>|CUSTOM1|</code> textarea, set the token:</p>



<pre class="wp-block-code"><code>|?MAX_CHILDREN=40|</code></pre>



<p class="wp-block-paragraph">That syntax is a template token override, not a config line. It tells the template renderer to use 40 instead of the default when it writes <code>pm.max_children</code> for that user. Writing <code>pm.max_children = 40</code> in that box instead is the single most common mistake here, and it does something worse than nothing: the template still emits its own <code>pm.max_children</code>, you now have the directive twice in one pool, and FPM refuses to start. On a server where one broken pool file stops the whole master, that takes down every site on the box.</p>



<p class="wp-block-paragraph">Rewrite and restart after any change, then confirm the value actually landed in the generated file rather than trusting the panel.</p>



<h2 class="wp-block-heading">Per-site PHP limits, and the difference that matters</h2>



<p class="wp-block-paragraph">The <code>|CUSTOM2|</code> textarea on the same page is where PHP settings go. The syntax is FPM pool syntax, not php.ini syntax:</p>



<pre class="wp-block-code"><code>php_admin_value&#91;memory_limit] = 512M
php_admin_value&#91;max_execution_time] = 120
php_value&#91;upload_max_filesize] = 64M
php_value&#91;post_max_size] = 64M
php_value&#91;max_input_vars] = 5000</code></pre>



<p class="wp-block-paragraph">The <code>admin</code> in <code>php_admin_value</code> is not decoration. Settings written with <code>php_value</code> can be raised at runtime by the application through <code>ini_set()</code>. Settings written with <code>php_admin_value</code> cannot. That distinction is the whole game on a multi-tenant server.</p>



<p class="wp-block-paragraph">WordPress and a good number of plugins will happily try to raise <code>memory_limit</code> at runtime, and some optimisation plugins push it to values that would let one bad request consume a large slice of the machine. If you want a genuine ceiling, use <code>php_admin_value</code>. If you want a sensible starting point that a developer can raise for a legitimate import job, use <code>php_value</code>. I use admin for memory and execution time, plain for upload sizes, because upload sizes are a support-ticket generator and not a stability risk.</p>



<p class="wp-block-paragraph"><code>max_input_vars</code> deserves its own note. It is the one that silently truncates a large WooCommerce product save or a menu with hundreds of items, with no error anywhere. The user reports &#8220;my changes did not save&#8221; and every log is clean. If you support content-heavy sites, raise it before someone asks.</p>



<h3 class="wp-block-heading">Letting site owners set their own</h3>



<p class="wp-block-paragraph">Under FPM there is no <code>.htaccess</code> route for PHP settings, because FPM is a separate process and never sees the file. DirectAdmin enables the per-directory alternative by default: a <code>.user.ini</code> file in <code>public_html</code>, controlled by the <code>user_ini.filename</code> setting. It handles the <code>PHP_INI_PERDIR</code> and <code>PHP_INI_USER</code> class of settings only, so it will never override anything you set with <code>php_admin_value</code>. That is the point of it.</p>



<h3 class="wp-block-heading">The WP-CLI gap</h3>



<p class="wp-block-paragraph">None of this applies to WP-CLI. Cron jobs and shell commands run through the CLI SAPI and read the CLI php.ini, not your pool. So a plugin update that works fine in the browser can die on memory in a cron-driven <code>wp cron event run</code>, and you will hunt through the pool config finding nothing wrong. Check the CLI limits separately when a scheduled task fails but the same action succeeds in wp-admin.</p>



<h2 class="wp-block-heading">OPcache is shared, and that is the invisible one</h2>



<p class="wp-block-paragraph">OPcache allocates its shared memory region when the FPM master starts, before any pool exists. Every pool under that master inherits the same region. You cannot meaningfully give one account its own OPcache budget through a pool directive, because by the time the pool is read the memory has already been carved out.</p>



<p class="wp-block-paragraph">Two consequences, and the second is the one nobody plans for.</p>



<p class="wp-block-paragraph">First, capacity. The default OPcache size is comfortable for one application and thin for thirty WordPress installs, each with its own core, theme and plugin tree. When it fills, OPcache starts evicting and eventually restarts, and every site on the version gets slower at once with nothing in any error log to explain it. Watch the wasted-memory and restart counters rather than guessing.</p>



<p class="wp-block-paragraph">Second, the sizing you actually need scales with the <em>total</em> number of PHP files across all accounts on that PHP version, not the biggest site. Consolidating everyone onto one PHP version looks tidy and quietly makes this worse. Spreading accounts across two or three PHP versions gives each master its own region, which is an underrated side effect of not forcing everyone to upgrade on the same day.</p>



<p class="wp-block-paragraph">OPcache is configured through CustomBuild rather than the pool. Edit the customised copy under CustomBuild&#8217;s <code>custom</code> directory so your changes survive updates, then rebuild and restart the FPM master.</p>



<h2 class="wp-block-heading">Making changes survive the next rebuild</h2>



<p class="wp-block-paragraph">Anything you type directly into a generated file is temporary. The next <code>rewrite_confs</code>, PHP update or panel update will flatten it, usually weeks later, and the regression will look like it came from nowhere.</p>



<p class="wp-block-paragraph">There are three durable places to put changes, in increasing order of blast radius:</p>



<ul class="wp-block-list">

<li><strong>Per user, all PHP versions.</strong> <code>/usr/local/directadmin/data/users/USERNAME/php/php-fpm.conf.custom1</code> and <code>.custom2</code>. These feed the same CUSTOM tokens as the panel textareas, so the settings follow the account when the user switches PHP version in Domain Setup. This is the one I reach for first.</li>


<li><strong>Server-wide, all users, all versions.</strong> <code>/usr/local/directadmin/data/templates/custom/php-fpm.conf.custom1</code> and <code>.custom2</code>. If either exists, its contents are injected into every pool file DirectAdmin writes. Good for a baseline you want everywhere.</li>


<li><strong>A full custom template.</strong> Copy <code>/usr/local/directadmin/data/templates/php-fpm.conf</code> to the <code>custom</code> subdirectory and edit it. This is the only route for pool directives that have no token, but you now own that file forever and stop receiving upstream template improvements. Use it when nothing else works, and leave a comment at the top saying why.</li>

</ul>



<p class="wp-block-paragraph">Global first, then per-user on top. Both land in the same token, so a per-user value does not replace a global one, it appends. For a directive that can only appear once in a pool, that is a duplicate and a failed restart.</p>



<h2 class="wp-block-heading">Troubleshooting</h2>



<h3 class="wp-block-heading">&#8220;server reached max_children setting&#8221;</h3>



<p class="wp-block-paragraph">The pool name in that warning tells you which account, not which site. Find the log first rather than guessing at a path:</p>



<pre class="wp-block-code"><code>grep -i 'error_log' /usr/local/php83/etc/php-fpm.conf</code></pre>



<p class="wp-block-paragraph">Before raising the limit, check whether the requests are legitimate. Cross-reference the timestamps against each domain&#8217;s access log in that account. Nine times out of ten it is one of three things: an uncached <code>admin-ajax.php</code> loop from a plugin, a bot crawling faceted search or filtered archive URLs that bypass page cache, or <code>wp-cron.php</code> firing on every request on a site with a heavy scheduled task. Raising <code>pm.max_children</code> to absorb bot traffic is paying for garbage with RAM.</p>



<h3 class="wp-block-heading">502 or 504 with no PHP error</h3>



<p class="wp-block-paragraph">The request is dying at the FPM boundary rather than inside PHP. Usual causes are a worker killed by <code>request_terminate_timeout</code>, a full listen queue, or the master failing to start after a config change. Check the FPM service status before you check anything in WordPress. If FPM did not start at all, the panel will still show your settings saved and every site on that PHP version will be down.</p>



<h3 class="wp-block-heading">Changes in the panel that do nothing</h3>



<p class="wp-block-paragraph">Read the generated per-user file after every change. If the value is not in there, the rewrite did not run or the token was wrong. If it is in there and PHP still reports something else, the master was not restarted, or something later in the pool is overriding it. A temporary <code>phpinfo()</code> file served by the site in question settles the argument in seconds. Delete it immediately afterwards.</p>



<h3 class="wp-block-heading">Session or temp files that are not where you left them</h3>



<p class="wp-block-paragraph">If PHP writes to <code>/tmp</code> and you cannot see the files over SSH, that is systemd&#8217;s <code>PrivateTmp</code> giving the FPM service its own private namespace. Nothing is broken. The files exist, in a mount only that process can see.</p>



<h3 class="wp-block-heading">disable_functions that will not budge</h3>



<p class="wp-block-paragraph"><code>disable_functions</code> is special. From a pool you can add to the list but not remove from it, because the php.ini value has already been applied. To allow a function for one account you have to stop setting it in php.ini at all and drive the whole thing from templates instead. DirectAdmin documents that pattern, and it is worth reading before you start rather than after you have half-done it, because the intermediate state removes protection server-wide.</p>



<h2 class="wp-block-heading">Common mistakes</h2>



<ul class="wp-block-list">

<li>Editing <code>/usr/local/phpXX/etc/php-fpm.conf</code> directly and being surprised when it reverts.</li>


<li>Writing <code>pm.max_children = 40</code> into a CUSTOM box instead of the <code>|?MAX_CHILDREN=40|</code> token, producing a duplicate directive and a dead master.</li>


<li>Sizing workers from a memory figure found online rather than measured on the box.</li>


<li>Using <code>php_value</code> for <code>memory_limit</code> on shared hosting, then wondering how a plugin raised it to 2G.</li>


<li>Assuming pool settings apply to WP-CLI and cron.</li>


<li>Raising limits to absorb bot traffic instead of blocking or caching it upstream.</li>


<li>Putting a client&#8217;s revenue site in the same account as their abandoned projects.</li>

</ul>



<h2 class="wp-block-heading">Best practices for tuning DirectAdmin PHP-FPM pools</h2>



<ul class="wp-block-list">

<li>One DirectAdmin user per site that matters. Isolation you get for free beats isolation you have to engineer.</li>


<li>Keep <code>ondemand</code> on shared servers. Only move to <code>dynamic</code> when a single busy site owns the box.</li>


<li>Set a global baseline in the custom template, then override per user. Do not hand-tune forty files.</li>


<li>Use <code>php_admin_value</code> for anything that protects the server and <code>php_value</code> for anything that is only a convenience.</li>


<li>Cut PHP requests before you add workers. Full-page caching, a CDN in front, and Cloudflare or similar handling bots removes far more load than any pool setting.</li>


<li>Disable WordPress&#8217;s request-triggered cron and drive it from a real system cron, so scheduled work stops competing with visitors for workers.</li>


<li>Alert on saturation, not just uptime. Worker utilisation per pool and PHP-FPM error log entries are the signals worth having in Netdata, Prometheus, or whatever you already run. A site that is queueing looks perfectly healthy to an HTTP check.</li>


<li>Re-measure after major plugin changes. A page builder or a new security plugin moves per-worker memory enough to invalidate your arithmetic.</li>

</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Frequently asked questions</h2>



<h3 class="wp-block-heading">Can I create a separate PHP-FPM pool for each domain in DirectAdmin?</h3>



<p class="wp-block-paragraph">Not through the panel. Pools are generated per user, per PHP version, and every domain in the account shares one. The supported ways to get separation are to move the domain to its own DirectAdmin user, or to enable isolated FPM mode for jailed users, which gives each user their own master process. Anything else means custom systemd units you maintain yourself, outside the panel&#8217;s knowledge.</p>



<h3 class="wp-block-heading">What is a good pm.max_children value for WordPress?</h3>



<p class="wp-block-paragraph">There is no portable number, because the input is your measured per-worker memory and your available RAM after the database. Measure the average resident size of your workers under real traffic, subtract everything else the server needs from total RAM, and divide. A lean brochure site and a WooCommerce store with thirty plugins can differ by a factor of three or more.</p>



<h3 class="wp-block-heading">Where does DirectAdmin store PHP-FPM pool configuration?</h3>



<p class="wp-block-paragraph">Generated per-user pools live at <code>/usr/local/directadmin/data/users/USERNAME/php/php-fpmXX.conf</code>, rendered from the template at <code>/usr/local/directadmin/data/templates/php-fpm.conf</code>. Do not edit either directly. Put durable changes in the custom files under the user&#8217;s <code>php</code> directory or in the <code>templates/custom</code> directory.</p>



<h3 class="wp-block-heading">Why do my PHP settings disappear after a CustomBuild update?</h3>



<p class="wp-block-paragraph">Because they were written into a generated file. CustomBuild and DirectAdmin regenerate those files on update and on any config rewrite. Only the custom template files and the CUSTOM token areas persist.</p>



<h3 class="wp-block-heading">Does .htaccess work for PHP settings under PHP-FPM?</h3>



<p class="wp-block-paragraph">No. FPM is a separate process from the web server and never reads <code>.htaccess</code>. Use a <code>.user.ini</code> file in the web root for the settings PHP allows at that level, or set the rest in the pool.</p>



<h3 class="wp-block-heading">Should I switch from ondemand to dynamic or static?</h3>



<p class="wp-block-paragraph">On a shared server, no. Ondemand exists precisely so that dozens of idle pools cost you nothing. On a server dedicated to one high-traffic site, <code>dynamic</code> removes cold-start latency and <code>static</code> gives the most predictable memory profile. The question to ask is whether your idle pools outnumber your busy ones.</p>



<h3 class="wp-block-heading">Can I give one WordPress site its own OPcache?</h3>



<p class="wp-block-paragraph">Not from the pool config. OPcache&#8217;s shared memory is allocated by the FPM master at startup and shared by every pool under it. The practical routes are isolated FPM mode, which gives jailed users their own master, or moving the site to a different PHP version so it sits under a different master.</p>



<h2 class="wp-block-heading">The one thing to take away</h2>



<p class="wp-block-paragraph">DirectAdmin PHP-FPM pools are a per-user boundary, not a per-site one. Every tuning decision that follows, how many workers to allow, whether a memory limit should be a suggestion or a wall, whether a client&#8217;s twelve domains belong in one account, comes out of that single fact.</p>



<p class="wp-block-paragraph">Measure your workers before you size them, use the CUSTOM token files so your work survives the next rebuild, and watch pool saturation rather than waiting for the 502 ticket. The site that goes down is rarely the site that caused it.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Need a hand with a DirectAdmin server?</h2>



<p class="wp-block-paragraph">Most of the work I do on DirectAdmin boxes is this kind of thing: sites that are fine in isolation and fall over together. If that sounds familiar, I can help with:</p>



<ul class="wp-block-list">

<li>Auditing every pool on a server and producing a worker budget that matches the RAM you actually have</li>


<li>Tracking down which domain in a shared account is saturating the pool, and why</li>


<li>Building a global custom template so per-user tuning stops being forty hand-edited files</li>


<li>Splitting a multi-site DirectAdmin account into isolated users without breaking mail, DNS or SSL</li>


<li>OPcache and per-version sizing on servers hosting many WordPress installs</li>


<li>Wiring up pool-level monitoring and alerts so saturation shows up before the ticket does</li>

</ul>



<p class="wp-block-paragraph">Send me a pool file, the output of that RSS command, or the last hour of your PHP-FPM error log, and I will tell you what I see.</p>



<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="https://www.upwork.com/freelancers/~01f15a912ad84a6620" target="_blank" rel="noreferrer noopener">Work with me on Upwork</a></div>
</div>
<p>The post <a href="https://john-nessime.com/blog/system-administration/directadmin-php-fpm-pools-wordpress-tuning/">DirectAdmin PHP-FPM Pools: Why One WordPress Site Takes Down the Others</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Nginx vs Apache vs Caddy: What Actually Decides It in Real Projects</title>
		<link>https://john-nessime.com/blog/devops/nginx-vs-apache-vs-caddy/</link>
		
		<dc:creator><![CDATA[John Nessime]]></dc:creator>
		<pubDate>Wed, 12 Aug 2026 18:00:00 +0000</pubDate>
				<category><![CDATA[DevOps]]></category>
		<category><![CDATA[System Administration]]></category>
		<category><![CDATA[Technical Guides]]></category>
		<category><![CDATA[Web Hosting]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[Automatic HTTPS]]></category>
		<category><![CDATA[Caddy]]></category>
		<category><![CDATA[Caddyfile]]></category>
		<category><![CDATA[Certbot]]></category>
		<category><![CDATA[Infrastructure]]></category>
		<category><![CDATA[Let's Encrypt]]></category>
		<category><![CDATA[Load Balancing]]></category>
		<category><![CDATA[mod_php]]></category>
		<category><![CDATA[MPM]]></category>
		<category><![CDATA[Nginx]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Production]]></category>
		<category><![CDATA[Reverse Proxy]]></category>
		<category><![CDATA[Sysadmin]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[VPS]]></category>
		<category><![CDATA[Web Performance]]></category>
		<category><![CDATA[Web Server]]></category>
		<guid isPermaLink="false">https://john-nessime.com/blog/?p=195</guid>

					<description><![CDATA[<p>Benchmarks pick the wrong winner. Here is how Nginx, Apache and Caddy actually differ in production: how each one fails under load, who owns certificate renewal, and where configuration lives when a developer needs to change it. Includes a decision procedure and the mistakes I see most.</p>
<p>The post <a href="https://john-nessime.com/blog/devops/nginx-vs-apache-vs-caddy/">Nginx vs Apache vs Caddy: What Actually Decides It in Real Projects</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A message landed in a channel I was in: &#8220;We&#8217;re standardising on Nginx.&#8221; No traffic profile. No mention of what the current setup was actually failing at. No list of the applications that would have to move. The decision got made because everybody knows Nginx is fast.</p>



<p class="wp-block-paragraph">That framing is where most of these arguments go sideways. On one box serving a normal website, all three of these servers will handle your traffic without breaking a sweat. The web server is almost never the slow part. The difference shows up months later, at 2am, in who gets paged and why.</p>



<p class="wp-block-paragraph">So this post compares <strong>Nginx vs Apache vs Caddy</strong> on the three things that genuinely change your on-call life: how each one behaves when it runs out of capacity, who owns the certificate lifecycle, and where configuration lives when someone other than you needs to change it. Then a decision procedure, the mistakes I keep running into, and an FAQ.</p>



<h2 class="wp-block-heading">Why the benchmark comparison picks the wrong winner</h2>



<p class="wp-block-paragraph">Almost every throughput comparison you&#8217;ll find measures static file serving on an idle machine with no application behind it. That is a real measurement of a thing that is not your bottleneck. If you&#8217;re proxying to PHP-FPM, Gunicorn, or a Node process, the request spends milliseconds in the web server and everything else in your code and your database.</p>



<p class="wp-block-paragraph">The honest exception: if you are running a CDN origin, terminating TLS at a high connection rate, or serving genuinely large volumes of static assets, the connection handling model does show up in your bill. That is a real workload and Nginx has earned its reputation there. It just isn&#8217;t most projects.</p>



<p class="wp-block-paragraph">For everyone else, the useful question is not &#8220;which is fastest&#8221; but &#8220;which chores am I choosing to keep.&#8221; That&#8217;s what the rest of this is about.</p>



<h2 class="wp-block-heading">Failure family one: the process model decides how you fall over</h2>



<p class="wp-block-paragraph">This is the invisible one. Nobody notices it during a migration and everybody notices it during a traffic spike.</p>



<h3 class="wp-block-heading">Apache: a hard worker ceiling, and a queue behind it</h3>



<p class="wp-block-paragraph">Apache&#8217;s behaviour depends entirely on which Multi-Processing Module is loaded. With <code>prefork</code>, you get one process per connection, capped by <code>MaxRequestWorkers</code>. When you hit that cap, new connections don&#8217;t get rejected. They sit in the kernel&#8217;s listen backlog waiting for a worker to free up.</p>



<p class="wp-block-paragraph">The symptom is the worst kind: the site &#8220;hangs.&#8221; Load average is unremarkable. The application log is quiet because the requests never reached the application. Your uptime check times out but a manual curl from the box works fine. People spend hours looking at the database.</p>



<p class="wp-block-paragraph">Check which MPM you&#8217;re actually running before you theorise about anything:</p>



<pre class="wp-block-code"><code># RHEL, AlmaLinux, Rocky
httpd -V | grep -i "Server MPM"

# Debian and Ubuntu
apache2ctl -V | grep -i "Server MPM"</code></pre>



<p class="wp-block-paragraph">The reason prefork survives is <code>mod_php</code>. It isn&#8217;t safe with Apache&#8217;s threaded MPMs, so PHP stacks historically pinned themselves to prefork and inherited its concurrency profile. The Apache project itself now points people at PHP-FPM over <code>mod_proxy_fcgi</code> instead, which frees you to run <code>mpm_event</code> and gets you HTTP/2 as a side effect, since <code>mod_http2</code> disables itself under prefork.</p>



<p class="wp-block-paragraph">If you inherit an Apache box, that migration is usually the single highest-value change available to you, and it does not require leaving Apache.</p>



<h3 class="wp-block-heading">Nginx: cheap connections, different limits</h3>



<p class="wp-block-paragraph">Nginx runs a small number of worker processes, each with an event loop. An idle keepalive connection costs a file descriptor and a little memory, not a process. Ten thousand of them is not a crisis.</p>



<p class="wp-block-paragraph">Its ceiling is <code>worker_processes</code> multiplied by <code>worker_connections</code>, plus whatever your file descriptor limit allows. When you hit it, Nginx says so plainly in the error log rather than silently queueing. That&#8217;s a genuine operational advantage: the failure announces itself.</p>



<p class="wp-block-paragraph">The trade is that when Nginx is proxying, saturation moves downstream. You stop seeing worker exhaustion and start seeing 502s and 504s from an overloaded PHP-FPM pool or app server. The web server is fine; you now have to go find the real limit.</p>



<h3 class="wp-block-heading">Caddy: goroutines, and a softer edge</h3>



<p class="wp-block-paragraph">Caddy is a Go binary and handles connections with goroutines that the runtime multiplexes onto OS threads. Practically, that sits between the other two: connections are cheap like Nginx, but you don&#8217;t tune a worker count at all.</p>



<p class="wp-block-paragraph">The failure mode is correspondingly softer and, honestly, harder to reason about. Instead of a clean &#8220;workers exhausted&#8221; line, you get memory growth and garbage collection pressure. There&#8217;s no knob you turned wrong, which is pleasant right up until you need to explain what happened.</p>



<h2 class="wp-block-heading">Failure family two: who owns the certificate lifecycle</h2>



<p class="wp-block-paragraph">This is the one that actually causes outages, and it is the sharpest genuine difference between the three.</p>



<p class="wp-block-paragraph">With Nginx or Apache, certificate management is a separate moving part. Certbot or another ACME client writes new files to disk on a timer, and the running server keeps serving whatever it loaded at startup. If nothing tells it to reload, the certificate on disk is valid and the certificate on the wire is expired. Users see a browser interstitial while your monitoring, if it checks the filesystem, reports everything fine.</p>



<p class="wp-block-paragraph">The fix is to attach the reload to the renewal itself, not to a separate cron entry that might drift:</p>



<pre class="wp-block-code"><code># Run the renewal end to end without touching real certificates.
# This exercises the challenge and the hook, which is the point.
certbot renew --dry-run

# Attach the reload to renewal so it only fires when a cert actually changed
certbot renew --deploy-hook "systemctl reload nginx"</code></pre>



<p class="wp-block-paragraph">The distinction that matters: <code>--deploy-hook</code> runs only when a certificate was actually renewed, so you&#8217;re not reloading the server twice a day for nothing. A blanket post-hook reloads regardless.</p>



<p class="wp-block-paragraph">Caddy removes this category of failure. Certificate issuance and renewal are built into the server itself, it swaps the certificate in memory when it renews, and there is no external hook to forget. Point several instances at the same storage backend and they coordinate rather than each hammering the CA. If you have ever been paged for an expired certificate, this alone is a serious argument.</p>



<p class="wp-block-paragraph">It is not free of foot-guns, though. On-demand TLS, where Caddy obtains a certificate during the handshake for a hostname it has never seen, is genuinely useful for multi-tenant setups where customers point their own domains at you. Enabled without restriction, it is an open door: anyone who points a DNS record at your IP can make you request a certificate. Caddy&#8217;s own documentation is blunt that it must be both enabled and restricted, and the mechanism is an <code>ask</code> endpoint you host:</p>



<pre class="wp-block-code"><code>{
    on_demand_tls {
        ask http://localhost:9123/ask
    }
}

https:// {
    tls {
        on_demand
    }
    reverse_proxy 127.0.0.1:3000
}</code></pre>



<p class="wp-block-paragraph">Caddy asks that endpoint whether a hostname is allowed before it will issue. It should be a fast indexed lookup against your own tenant table, returning in milliseconds, because a TLS handshake is being held open while it answers. The older <code>interval</code> and <code>burst</code> rate-limiting options are no longer recommended and should be removed if you still have them in a config.</p>



<p class="wp-block-paragraph">One more Caddy default worth knowing: the admin API listens on localhost port 2019 and accepts a full config replacement without authentication. That is fine bound to loopback and catastrophic if you helpfully expose it while debugging a container.</p>



<h2 class="wp-block-heading">Failure family three: where the configuration lives</h2>



<p class="wp-block-paragraph">This is the axis people underweight, and it&#8217;s usually the one that actually decides the answer.</p>



<p class="wp-block-paragraph">Apache supports <code>.htaccess</code>: per-directory configuration, read at request time, changeable by anything that can write to the directory. No reload, no root access. That is why the entire shared hosting world is built on Apache, and why control panels like cPanel and DirectAdmin, and hosts like InterServer, assume it. A WordPress plugin can rewrite its own rules and it just works.</p>



<p class="wp-block-paragraph">The costs are real and they&#8217;re the mirror image of the benefit. With overrides enabled, Apache walks the directory tree looking for <code>.htaccess</code> files on every single request. And your security rules can be silently overridden by whatever gets deployed into a directory, by anyone who can deploy.</p>



<p class="wp-block-paragraph">Nginx has no equivalent and this is deliberate. Everything is central, and a change requires a reload. Rewrites are handled with <code>try_files</code> rather than per-directory rule files. If you own the box, that&#8217;s a feature: nothing changes routing behind your back and the running configuration is exactly what&#8217;s in version control. If you host other people&#8217;s applications, it&#8217;s a support burden, because every customer rewrite rule becomes a ticket for you.</p>



<p class="wp-block-paragraph">Caddy is central too, either a Caddyfile or JSON. The difference is that config changes can go through the admin API as an atomic swap rather than a file edit plus a signal, which suits automated pipelines nicely.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Nginx: where it wins and where it doesn&#8217;t</h2>



<p class="wp-block-paragraph"><strong>Where it wins:</strong> high connection counts on modest hardware, static and cached content, TLS termination in front of an application fleet, and the sheer weight of the ecosystem. Every stack you&#8217;ll meet has a known-good Nginx config, and everyone you hire has already read one.</p>



<p class="wp-block-paragraph"><strong>Where it doesn&#8217;t:</strong> the DNS behaviour catches people constantly. A hostname written directly into <code>proxy_pass</code>, or into an <code>upstream</code> block, is resolved once at startup and cached for the lifetime of the process. When your upstream is a cloud load balancer or a container that moves, Nginx keeps sending traffic to an IP that stopped being yours. Nothing in the log says &#8220;stale DNS&#8221;; you just get connection timeouts to an address that looks plausible.</p>



<p class="wp-block-paragraph">Forcing re-resolution means putting the name in a variable and giving Nginx a resolver, because that&#8217;s what moves the lookup from config parse time to request time:</p>



<pre class="wp-block-code"><code>location / {
    # resolver moves lookups to runtime; valid= caps how long a result is reused
    resolver 127.0.0.53 valid=30s ipv6=off;

    # the variable is the trick: it defers evaluation to per-request
    set $upstream_host app.internal.example.com;
    proxy_pass http://$upstream_host:8080$request_uri;
}</code></pre>



<p class="wp-block-paragraph">Note the <code>$request_uri</code> on the end. Once a variable appears in <code>proxy_pass</code>, Nginx stops doing its usual URI rewriting for you, so you have to pass the path explicitly. Miss that and every request lands on the upstream&#8217;s root. Also worth knowing: an <code>upstream</code> block defined elsewhere with the same name will take precedence over the runtime lookup, which produces a very confusing afternoon.</p>



<p class="wp-block-paragraph">The other honest downside is governance. Nginx is owned by F5, and a longtime core developer forked it to freenginx after a dispute over how security issues were being handled, with an earlier fork, Angie, already in existence. Mainline Nginx remains overwhelmingly dominant and I would not choose a fork for a client project today. But &#8220;who decides what ships&#8221; is a fair question to ask about any dependency, and this one now has a public answer.</p>



<h2 class="wp-block-heading">Apache: where it wins and where it doesn&#8217;t</h2>



<p class="wp-block-paragraph"><strong>Where it wins:</strong> anywhere applications need to configure the web server without touching the web server. Shared hosting, multi-tenant WordPress, legacy applications that ship their own rewrite rules. The module ecosystem is enormous and modules load at runtime rather than requiring a rebuild. And the documentation is, quietly, the best of the three: nearly every directive has a page explaining its interactions.</p>



<p class="wp-block-paragraph"><strong>Where it doesn&#8217;t:</strong> memory per connection, if you&#8217;re still on prefork. Slow clients holding processes open is the specific scenario that hurts, because a process that&#8217;s waiting on a phone on bad mobile data is a process nobody else can use. Putting a CDN or a proxy in front absorbs a lot of that, which is a large part of why Apache origins behind Cloudflare behave so much better than bare ones.</p>



<p class="wp-block-paragraph">I would not migrate a working Apache installation to Nginx purely for performance. I would move it from prefork and <code>mod_php</code> to <code>mpm_event</code> and PHP-FPM first, then measure, and quite often stop there.</p>



<h2 class="wp-block-heading">Caddy: where it wins and where it doesn&#8217;t</h2>



<p class="wp-block-paragraph"><strong>Where it wins:</strong> anything where certificate management is the operational risk. Small teams without a dedicated ops person, internal tooling with a long tail of hostnames nobody tracks, multi-tenant products serving customer-owned domains, container stacks where a single static binary with no runtime dependencies is a real simplification. A working HTTPS reverse proxy is genuinely a few lines.</p>



<pre class="wp-block-code"><code>app.example.com {
    encode zstd gzip
    reverse_proxy app:3000
}</code></pre>



<p class="wp-block-paragraph">That obtains a certificate, redirects HTTP to HTTPS, negotiates compression and proxies, with no ACME client, no cron, no reload hook.</p>



<p class="wp-block-paragraph"><strong>Where it doesn&#8217;t:</strong> the ecosystem gap is real. When something unusual breaks, there are fewer Stack Overflow answers and fewer engineers who have seen it before. Adding a plugin means rebuilding the binary with <code>xcaddy</code> rather than dropping in a module, which is fine in CI and annoying on a box you SSH into. And there&#8217;s a subtler cost: automation you don&#8217;t understand is still automation you&#8217;re responsible for. Being paged about ACME behaviour you&#8217;ve never had to learn is a worse afternoon than being paged about Certbot.</p>



<p class="wp-block-paragraph">On upstream DNS, Caddy dials static upstreams by name so resolution happens at dial time rather than being frozen at config load. When the set of addresses changes, the <code>dynamic a</code> upstream module with a <code>refresh</code> interval is the built-in answer, which is a cleaner story than Nginx&#8217;s variable trick.</p>



<h2 class="wp-block-heading">A decision procedure for Nginx vs Apache vs Caddy</h2>



<p class="wp-block-paragraph">Work down this list and stop at the first one that matches. It&#8217;ll be right more often than a benchmark.</p>



<ol class="wp-block-list">
<li><strong>Do applications you don&#8217;t control need to change server behaviour?</strong> If yes, Apache. Nothing else has <code>.htaccess</code>, and reimplementing it in tickets is not a plan.</li>

<li><strong>Is TLS renewal your top operational risk?</strong> Small team, no dedicated ops, lots of hostnames, or customer-owned domains: Caddy. It deletes a whole class of incident.</li>

<li><strong>Are you terminating a very high connection rate, or serving heavy static content?</strong> Nginx. This is the workload it was designed around.</li>

<li><strong>Does the team already run one of these well?</strong> Keep it. Operational familiarity beats a 20% throughput difference you were never going to measure.</li>

<li><strong>Is there a working Apache install and no specific complaint?</strong> Fix the MPM and PHP handler, then reassess. Most &#8220;we need Nginx&#8221; tickets are actually &#8220;we need to stop using prefork.&#8221;</li>

<li><strong>None of the above?</strong> Nginx for anything that will be handed to another engineer. Caddy for anything you&#8217;ll run yourself.</li>
</ol>



<h2 class="wp-block-heading">Common mistakes</h2>



<ul class="wp-block-list">
<li><strong>Renewing certificates without reloading.</strong> The single most common self-inflicted TLS outage on Nginx and Apache. Use a deploy hook and verify it with a dry run.</li>

<li><strong>Migrating for benchmark numbers.</strong> You inherit a new failure mode and a team that has never debugged it, in exchange for latency nobody could perceive.</li>

<li><strong>Leaving Apache on prefork after moving to PHP-FPM.</strong> The migration is only half done, and you&#8217;re still paying prefork&#8217;s memory cost while also giving up HTTP/2.</li>

<li><strong>Hostnames baked into <code>proxy_pass</code> with dynamic upstreams.</strong> Works perfectly until the upstream IP changes, then fails in a way that looks like a network problem.</li>

<li><strong>Enabling Caddy&#8217;s on-demand TLS without an <code>ask</code> endpoint.</strong> You&#8217;ve handed strangers the ability to make you request certificates.</li>

<li><strong>Exposing Caddy&#8217;s admin API beyond loopback.</strong> Unauthenticated full config replacement is exactly as bad as it sounds.</li>

<li><strong>Monitoring the certificate file instead of the handshake.</strong> The file is almost never the thing that&#8217;s wrong. Check what the server actually presents, from outside the box.</li>
</ul>



<h2 class="wp-block-heading">How I&#8217;d decide, in practice</h2>



<ul class="wp-block-list">
<li>Pick based on who has to change the config six months from now, not on requests per second today.</li>

<li>Whichever you choose, dump the effective configuration rather than reading files: <code>nginx -T</code>, <code>apachectl -S</code>, <code>caddy adapt --config /etc/caddy/Caddyfile</code>. Include files hide surprises.</li>

<li>Monitor TLS expiry as an external handshake check from a service like Better Stack or a Grafana Cloud synthetic, not as a filesystem check on the box.</li>

<li>Put something in front. A CDN like Cloudflare or Fastly absorbs slow clients and connection floods, which flattens most of the architectural differences here.</li>

<li>Run one server type per environment. Mixing Nginx in staging and Apache in production means your rewrite rules are untested by definition.</li>

<li>Validate before reloading, every time. <code>nginx -t</code>, <code>apachectl configtest</code> and <code>caddy validate</code> all exist so you don&#8217;t find out from users.</li>
</ul>



<h2 class="wp-block-heading">Frequently asked questions</h2>



<h3 class="wp-block-heading">Is Caddy production ready?</h3>



<p class="wp-block-paragraph">Yes. It&#8217;s been running real workloads for years and its certificate automation removes more operational risk than its smaller ecosystem adds, for most teams. The caveat is support surface, not stability: when you hit something unusual, there are fewer people who have hit it before you.</p>



<h3 class="wp-block-heading">Can I run Nginx and Apache together?</h3>



<p class="wp-block-paragraph">Yes, and it&#8217;s a common pattern: Nginx on 80 and 443 handling TLS and static files, proxying dynamic requests to Apache on a local port so applications keep their <code>.htaccess</code> behaviour. You get Nginx&#8217;s connection handling and Apache&#8217;s per-directory config. The cost is two servers to patch, two logs to correlate, and real client IP to forward correctly.</p>



<h3 class="wp-block-heading">Which is best for WordPress?</h3>



<p class="wp-block-paragraph">Apache if plugins need to write rewrite rules, which on a typical multi-plugin site they do. Nginx if you control the site and are willing to translate those rules into <code>try_files</code> and static location blocks once. Plenty of high-traffic WordPress runs on Nginx; it just means plugin-generated rules are your job now.</p>



<h3 class="wp-block-heading">Does Nginx really re-resolve DNS for upstreams?</h3>



<p class="wp-block-paragraph">Not by default. A literal hostname in <code>proxy_pass</code> or an <code>upstream</code> block is resolved at startup and reused indefinitely, regardless of TTL. You get runtime resolution by using a variable together with a <code>resolver</code> directive, and remembering that this changes how the request URI is passed.</p>



<h3 class="wp-block-heading">Should I switch from mod_php to PHP-FPM?</h3>



<p class="wp-block-paragraph">In almost all cases yes. It&#8217;s the change that unlocks Apache&#8217;s threaded MPMs and HTTP/2, and it separates PHP&#8217;s lifecycle from the web server&#8217;s so you can restart one without the other. Check first for anything relying on PHP settings in Apache config or <code>.htaccess</code>, since those move to a <code>.user.ini</code> file with different syntax.</p>



<h3 class="wp-block-heading">Does the freenginx fork mean I should stop using Nginx?</h3>



<p class="wp-block-paragraph">No. Mainline Nginx under F5 remains the overwhelmingly dominant option with the broadest packaging and support. The fork is worth knowing about as context on project governance rather than as a migration target, and I wouldn&#8217;t put a client on a fork without a specific reason.</p>



<h3 class="wp-block-heading">Which uses the least memory?</h3>



<p class="wp-block-paragraph">Nginx, generally, with Caddy close behind and Apache furthest away when running prefork. But the gap narrows sharply once Apache is on <code>mpm_event</code>, and on a VPS from a provider like Hetzner or DigitalOcean the memory your application uses will dwarf all three. Measure your own stack before treating this as a deciding factor.</p>



<h2 class="wp-block-heading">The one thing worth remembering</h2>



<p class="wp-block-paragraph">The <strong>Nginx vs Apache vs Caddy</strong> question is not a performance question. All three are fast enough for what you&#8217;re doing. What differs is which operational chores each one hands back to you: Apache gives applications control at the cost of memory and predictability, Nginx gives you central control at the cost of owning every rewrite rule and the certificate reload, and Caddy takes the certificate lifecycle off your plate at the cost of a smaller ecosystem when things get strange.</p>



<p class="wp-block-paragraph">Pick the set of chores your team can actually carry. Then go fix the thing that&#8217;s genuinely slow, which is almost certainly not the web server.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Need a second opinion on your web server setup?</h2>



<p class="wp-block-paragraph">Most of the work I do here is unglamorous and pays for itself quickly. Things I can help with:</p>



<ul class="wp-block-list">
<li>Auditing an existing Nginx, Apache or Caddy config and telling you what will break under load before it does</li>

<li>Migrating Apache from prefork and <code>mod_php</code> to <code>mpm_event</code> with PHP-FPM, without breaking existing <code>.htaccess</code> behaviour</li>

<li>Translating a working Apache rewrite set into an Nginx configuration, with a test plan so you know it matches</li>

<li>Fixing certificate renewal that runs but never reloads, including deploy hooks and external expiry monitoring</li>

<li>Setting up Caddy for multi-tenant or customer-domain hosting with on-demand TLS properly restricted</li>

<li>Diagnosing intermittent 502s, 504s and stale-upstream timeouts that look like network faults and aren&#8217;t</li>
</ul>



<p class="wp-block-paragraph">Send me your config, an error log excerpt, or the output of <code>nginx -T</code> and tell me what you&#8217;re seeing. I&#8217;d rather look at the real thing than guess from a description.</p>



<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="https://www.upwork.com/freelancers/~01f15a912ad84a6620" target="_blank" rel="noreferrer noopener">Work with me on Upwork</a></div>
</div>
<p>The post <a href="https://john-nessime.com/blog/devops/nginx-vs-apache-vs-caddy/">Nginx vs Apache vs Caddy: What Actually Decides It in Real Projects</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Managed WordPress Hosting vs VPS: Who Owns the Failure?</title>
		<link>https://john-nessime.com/blog/wordpress/managed-wordpress-hosting-vs-vps/</link>
		
		<dc:creator><![CDATA[John Nessime]]></dc:creator>
		<pubDate>Sat, 08 Aug 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[Hosting & Infrastructure]]></category>
		<category><![CDATA[Web Hosting]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[Cloudflare]]></category>
		<category><![CDATA[Cost Optimization]]></category>
		<category><![CDATA[Managed Hosting]]></category>
		<category><![CDATA[Object Cache]]></category>
		<category><![CDATA[Redis]]></category>
		<category><![CDATA[Restore Testing]]></category>
		<category><![CDATA[Self Hosting]]></category>
		<category><![CDATA[Sysadmin]]></category>
		<category><![CDATA[Vendor Lock-In]]></category>
		<category><![CDATA[VPS]]></category>
		<category><![CDATA[Website Migration]]></category>
		<category><![CDATA[WordPress Backup]]></category>
		<category><![CDATA[WordPress Hosting]]></category>
		<category><![CDATA[WP-CLI]]></category>
		<guid isPermaLink="false">https://john-nessime.com/blog/?p=165</guid>

					<description><![CDATA[<p>Managed WordPress hosting and a self-managed VPS can both serve a fast site. They differ on which failures land on you. A practical comparison covering visit metering, disallowed plugin policies, silent backup failure, patching debt, and a cost model that survives contact with reality.</p>
<p>The post <a href="https://john-nessime.com/blog/wordpress/managed-wordpress-hosting-vs-vps/">Managed WordPress Hosting vs VPS: Who Owns the Failure?</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A colleague pinged me about a site where the caching plugin had stopped existing. Not deactivated. Gone from the plugin list, gone from the filesystem. Nobody on the team had touched it.</p>



<p class="wp-block-paragraph">The host had removed it. It was on their disallowed list, a routine scan found it, and the platform did exactly what its documentation said it would do. Everyone had agreed to that when they signed up. Nobody had read it.</p>



<p class="wp-block-paragraph">That incident is the whole managed WordPress hosting vs VPS argument in miniature, and it has almost nothing to do with page speed. Both models can serve a fast WordPress site. They differ on which failures land on you, which land on someone else, and which ones stay invisible until the worst possible moment.</p>



<p class="wp-block-paragraph">This post compares the two the way I&#8217;d compare them for a client: an honest profile of each, the costs that never appear on the invoice, a procedure for modelling the real number, and the decision rules I actually use. No benchmark charts, because your theme and your plugin count will dominate anything the host does.</p>



<h2 class="wp-block-heading">The real question: who owns the failure at 2am</h2>



<p class="wp-block-paragraph">Most comparisons frame this as control versus convenience. True, but useless, because it doesn&#8217;t tell you what to buy. The framing that predicts regret is: when something breaks, whose problem is it, and how fast can they fix it?</p>



<p class="wp-block-paragraph">Split the failure surface into layers and the answer gets concrete:</p>



<ul class="wp-block-list">
<li><strong>Hardware and hypervisor.</strong> Theirs in both models. Your recovery time is still yours.</li>

<li><strong>OS, web server, PHP, database.</strong> Theirs on managed hosting, yours on an unmanaged VPS. This is the big split.</li>

<li><strong>Backups and restores.</strong> Theirs on managed, and tested, because their business depends on it. Yours on a VPS, and almost nobody tests them.</li>

<li><strong>Core, themes, plugins.</strong> Mostly yours either way. Some hosts auto-update, but the compatibility fallout is still yours.</li>

<li><strong>Your code and your content.</strong> Always yours. No host takes this on.</li>
</ul>



<p class="wp-block-paragraph">The bottom two rows never move. A managed platform will not make your bloated page builder fast, and it will not stop a vulnerable plugin from being exploited. It moves the middle three rows off your plate and charges you in money and in flexibility.</p>



<h2 class="wp-block-heading">Managed WordPress hosting, assessed honestly</h2>



<h3 class="wp-block-heading">Where it genuinely wins</h3>



<p class="wp-block-paragraph">The strongest argument isn&#8217;t performance. It&#8217;s that the boring, high-consequence work gets done by people whose job it is, on a schedule, whether or not you remember.</p>



<ul class="wp-block-list">
<li><strong>Restores that work.</strong> One-click recovery from a backup the platform verifies. That&#8217;s the difference between a bad hour and a bad week.</li>

<li><strong>Server-level page caching, already tuned.</strong> WP Engine, Kinsta, Pressable and Flywheel run full-page caching in front of PHP. It&#8217;s why they ban plugins that try to do the same thing badly.</li>

<li><strong>Staging that isn&#8217;t a science project.</strong> Clone, test, push back. On a VPS that&#8217;s yours to build, and a half-built staging setup is worse than none.</li>

<li><strong>Support that knows WordPress.</strong> A generic VPS provider&#8217;s ticket ends at &#8220;the server is up&#8221;. A WordPress host will look at your slow query log.</li>

<li><strong>Edge protection before PHP loads.</strong> WAF and bot filtering at the infrastructure layer, the only layer where they meaningfully help. A plugin firewall runs after the request has already cost you a PHP worker.</li>
</ul>



<p class="wp-block-paragraph">If nobody on your team wants to own a Linux box, that list is decisive. The rest of this section is about what you&#8217;re trading away.</p>



<h3 class="wp-block-heading">Visit metering is a billing model, not a traffic report</h3>



<p class="wp-block-paragraph">This is the cost that bites hardest, because it doesn&#8217;t look like a technical constraint until the invoice arrives.</p>



<p class="wp-block-paragraph">Most managed plans meter &#8220;visits&#8221; rather than bandwidth or CPU, and the definition is narrower than it sounds. <a href="https://wpengine.com/support/count-visits/" target="_blank" rel="noreferrer noopener">WP Engine defines a billable visit</a> as one unique IP address logged per day, with static assets like images, CSS and JavaScript excluded, and known and suspected bot user agents filtered out. <a href="https://kinsta.com/docs/billing/wordpress-hosting-plans/overages/" target="_blank" rel="noreferrer noopener">Kinsta counts</a> the sum of unique IPs seen within each 24-hour period across the plan, and also sells bandwidth-metered plans as an alternative. Flywheel filters IPs belonging to known bots, spammers and attackers before counting.</p>



<p class="wp-block-paragraph">Three consequences follow, and they&#8217;re the ones people miss:</p>



<ul class="wp-block-list">
<li><strong>Your host&#8217;s number will not match Google Analytics.</strong> Analytics counts sessions from browsers that ran its JavaScript. Your host counts IPs that hit the server. Arguing about the gap is wasted effort.</li>

<li><strong>Bot filtering is best-effort.</strong> Hosts filter what they can identify. A scraper with a browser-shaped user agent and rotating IPs looks, to the meter, like a crowd of new visitors.</li>

<li><strong>Overages are charged per block of excess visits.</strong> Rates change, so check the current figure rather than trusting any article, including this one. What matters is the shape: a viral post or a bad crawl month produces a bill you didn&#8217;t budget for.</li>
</ul>



<p class="wp-block-paragraph">A VPS has the opposite failure mode. Bad traffic costs you CPU and RAM, not money, right up until it exhausts them and the site falls over. One model converts load into a bill, the other converts it into an outage. Pick the one you&#8217;d rather explain to whoever owns the site.</p>



<h3 class="wp-block-heading">The plugin policy is a real constraint</h3>



<p class="wp-block-paragraph">Managed hosts publish disallowed plugin lists and they enforce them. WP Engine documents that disallowed plugins are found by periodic scans of the site&#8217;s filesystem, the owner is notified, and the plugin is removed. That&#8217;s not a threat, it&#8217;s the operating model, and it&#8217;s why the incident I opened with was nobody&#8217;s fault.</p>



<p class="wp-block-paragraph">The banned categories are consistent across vendors, and the reasoning holds up in each case:</p>



<ul class="wp-block-list">
<li><strong>Caching plugins.</strong> They fight the platform&#8217;s own page cache and produce stale output that support then has to debug.</li>

<li><strong>Backup plugins.</strong> They write large archives onto the same disk the site runs on, which is a useless backup if the disk is what fails.</li>

<li><strong>Database optimisation plugins.</strong> Long-running write-heavy queries on shared infrastructure affect neighbours.</li>

<li><strong>Some security plugins, or specific features within them.</strong> Filesystem-based rule storage and high-volume traffic logging are the usual triggers. Policies vary a lot here.</li>

<li><strong>Plugins that send mail directly.</strong> Platforms push you to a transactional provider instead, for deliverability reasons that are genuinely in your interest.</li>
</ul>



<p class="wp-block-paragraph">None of that is unreasonable. The problem is when a plugin your business depends on lands in one of those buckets, or when a host adds something to the list later. Before migrating, read the current disallowed list for that specific host and diff it against your live active plugins. Ten minutes, and it&#8217;s the highest-value thing you can do in a hosting evaluation.</p>



<h3 class="wp-block-heading">You get one shape of application</h3>



<p class="wp-block-paragraph">A managed WordPress platform runs WordPress. If your project also needs a Node service, a Python worker, a queue, or a self-hosted analytics instance, that&#8217;s a second bill somewhere else. On a VPS those things are free in cash terms because you already own the machine. They cost you memory and attention instead. Whether that&#8217;s a good trade depends on whether you were going to need them anyway.</p>



<h2 class="wp-block-heading">Running WordPress on your own VPS, assessed honestly</h2>



<h3 class="wp-block-heading">Where it genuinely wins</h3>



<ul class="wp-block-list">
<li><strong>Resources per unit of money.</strong> Hetzner, InterServer, DigitalOcean and Vultr will sell you more CPU and RAM than an entry managed plan. Whether you can use it is a separate question.</li>

<li><strong>Many sites, one bill.</strong> This is where the economics genuinely flip. Ten small sites on one adequate VPS is one server to patch. Ten managed plans is ten invoices.</li>

<li><strong>No metering surprises.</strong> Traffic spikes hit your load average, not your credit card.</li>

<li><strong>You choose the stack.</strong> Nginx or Apache, PHP-FPM pool sizing, Redis for the object cache, your own MariaDB tuning and firewall rules. If the platform&#8217;s opinions were what limited you, this is the fix.</li>

<li><strong>No lock-in.</strong> A tarball and a database dump move anywhere. Platform-specific caching behaviour, redirect rules and deployment workflows do not.</li>
</ul>



<h3 class="wp-block-heading">The failure that stays invisible: the backup you never restored</h3>



<p class="wp-block-paragraph">Here&#8217;s the one that actually ends projects. Backups on a self-managed VPS fail silently. The cron entry runs, the exit code goes nowhere, the destination bucket quietly rejects writes after a credential rotation, and nothing tells you. You find out the day you need it, which is also the day you have no other option.</p>



<p class="wp-block-paragraph">The fix isn&#8217;t a better tool. Restic and BorgBackup are both excellent and neither will save you. The fix is a restore drill on a schedule, into a scratch directory, where you verify the data is actually there.</p>



<pre class="wp-block-code"><code># Restore the newest snapshot into a scratch path, never over the live site
restic -r /srv/backups/wp restore latest --target /tmp/restore-drill

# Confirm the dump is real and not a zero-byte file or a truncated
# write from a run that died halfway through
ls -lh /tmp/restore-drill/db/
head -n 20 /tmp/restore-drill/db/site.sql

# Load into a throwaway database and count what came back.
# If wp_posts is empty, your backups have been failing for weeks.
mysql -u drill -p drill_db &lt; /tmp/restore-drill/db/site.sql
mysql -u drill -p -e "SELECT COUNT(*) FROM drill_db.wp_posts;"</code></pre>



<p class="wp-block-paragraph">Adjust the repository path and database names to your own. The shape is the point: restore somewhere harmless, then check a row count that would be non-zero on a healthy site. A backup you haven&#8217;t restored is a hypothesis, not a backup. Uptime checks from something like UptimeRobot tell you the site is down; they tell you nothing about whether you can get it back.</p>



<h3 class="wp-block-heading">Patching debt compounds quietly</h3>



<p class="wp-block-paragraph">The second invisible cost is the gap between &#8220;updates are available&#8221; and &#8220;updates are applied and the affected services have actually restarted&#8221;. Upgrading a package without restarting the process leaves the old, vulnerable code resident in memory. That&#8217;s why security scanners and package managers disagree so often.</p>



<pre class="wp-block-code"><code># RHEL family (AlmaLinux, Rocky, CentOS Stream)
# What security updates are outstanding?
sudo dnf updateinfo list security

# After applying them: does anything still need a restart?
# Exit 0 means no reboot required, exit 1 means reboot required.
sudo dnf needs-restarting -r

# Debian and Ubuntu: what would unattended-upgrades actually do?
sudo unattended-upgrade --dry-run --debug</code></pre>



<p class="wp-block-paragraph">Run those on any VPS you inherited from someone else. The output is usually educational. Then there&#8217;s the WordPress layer, which stays yours regardless of hosting model:</p>



<pre class="wp-block-code"><code># Do the core files match the official checksums for this version?
# A mismatch means a modified core file or an active compromise.
wp core verify-checksums

# How many plugins are behind?
wp plugin list --update=available --format=count

# Which tables are eating the disk? Usually wp_options, wp_postmeta,
# or a logging plugin nobody remembers installing.
wp db size --tables --format=table</code></pre>



<h2 class="wp-block-heading">Model the cost before you argue about it</h2>



<p class="wp-block-paragraph">Sticker price comparisons are how people talk themselves into the wrong answer in both directions. This procedure produces a number you can defend.</p>



<ol class="wp-block-list">
<li><strong>Count your sites.</strong> One tilts toward managed. Five or more tilts toward a VPS, because admin time is close to fixed and plan fees are per-site.</li>

<li><strong>Estimate your metered visit count from your own logs.</strong> If you&#8217;re already on a server you control, you can approximate what a metered host would bill you before signing anything.</li>

<li><strong>Price the VPS honestly.</strong> Instance plus block storage plus offsite backup destination plus any control panel licence. The advertised instance price is rarely the whole line.</li>

<li><strong>Put a rate on your own hours.</strong> Whatever you&#8217;d bill a client, or pay someone else. Zero is not an honest number.</li>

<li><strong>Add the one-off build.</strong> Standing up a hardened WordPress VPS with TLS, firewall, object cache, tested backups and log rotation is a solid chunk of focused work the first time. Amortise it over twelve months.</li>

<li><strong>Add your recovery cost.</strong> Hours to rebuild from scratch, times your rate, times how often you honestly think it&#8217;ll happen. This term is what makes managed hosting look cheap for a single revenue-generating site.</li>
</ol>



<p class="wp-block-paragraph">Step two is worth doing properly, because it&#8217;s the number nobody has. On Nginx with the standard combined log format, this gets you close:</p>



<pre class="wp-block-code"><code># Unique client IPs in the log. A rough upper bound on metered "visits".
awk '{print $1}' /var/log/nginx/access.log | sort -u | wc -l

# Closer to how hosts count: ignore static assets, since most platforms
# exclude images, CSS and JS from billable visits.
awk '$7 !~ /.(css|js|png|jpg|jpeg|gif|svg|webp|woff2?|ico)$/ {print $1}' 
  /var/log/nginx/access.log | sort -u | wc -l

# What is actually hitting you? User agents by request count.
# If the top entries are crawlers, that is your overage risk.
awk -F'"' '{print $6}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20</code></pre>



<p class="wp-block-paragraph">Field positions assume the combined log format; adjust the column numbers if you&#8217;ve customised <code>log_format</code>. The third command is the interesting one. If crawlers dominate your traffic, visit-metered hosting will charge you for whatever the host&#8217;s filtering misses, and that&#8217;s a risk you can quantify before signing up rather than after.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">The middle ground worth looking at first</h2>



<p class="wp-block-paragraph">This gets presented as two options. There are four, and the middle two get skipped far too often.</p>



<ul class="wp-block-list">
<li><strong>Managed VPS.</strong> InterServer, Liquid Web and others sell VPS plans where the provider handles OS patching, firewall and backups while you keep root. Read the scope: &#8220;managed&#8221; often stops at the operating system and leaves the WordPress stack to you.</li>

<li><strong>Control panel on your own VPS.</strong> DirectAdmin, cPanel or CyberPanel gives you WordPress-aware tooling, staging and one-click TLS on infrastructure you still own. You pay a licence and give up some configuration purity.</li>

<li><strong>Server management platforms.</strong> Cloudways, RunCloud, GridPane and SpinupWP provision and manage a stack on a VPS you rent from a provider of your choice. Managed-style workflows without the per-visit meter.</li>

<li><strong>A CDN in front of anything.</strong> Putting Cloudflare or a similar edge ahead of a modest VPS absorbs a large share of the traffic that would otherwise push you onto a bigger plan. Cheapest performance win available to either model, and frequently the actual answer to &#8220;we need better hosting&#8221;.</li>
</ul>



<p class="wp-block-paragraph">If you&#8217;re leaning toward a VPS mainly because managed pricing looks steep, look at options two and three before committing to a bare Linux box.</p>



<h2 class="wp-block-heading">How I&#8217;d decide between managed WordPress hosting vs VPS</h2>



<p class="wp-block-paragraph">These are the rules I apply, in order. The first match usually settles it.</p>



<ul class="wp-block-list">
<li><strong>Nobody on the team can or wants to run Linux.</strong> Managed, and it isn&#8217;t close. An unpatched VPS is worse than the most restrictive managed plan.</li>

<li><strong>One site, and it generates revenue.</strong> Managed. The recovery term dominates everything else in the cost model.</li>

<li><strong>Five or more sites and someone competent to run them.</strong> VPS, or a management platform on top of one.</li>

<li><strong>You need services WordPress isn&#8217;t.</strong> VPS. Don&#8217;t pay for two platforms to avoid learning one.</li>

<li><strong>A plugin you can&#8217;t remove is on the host&#8217;s disallowed list.</strong> VPS, or a different managed host. Do not migrate hoping for an exception.</li>

<li><strong>Traffic is spiky and crawler-heavy.</strong> Lean VPS, or pick a bandwidth-metered plan over a visit-metered one. Metering punishes exactly this traffic shape.</li>

<li><strong>You&#8217;re mostly annoyed at the current bill.</strong> Neither, yet. Put a CDN in front, cut the plugin count, fix the object cache. Hosting is often not the bottleneck.</li>
</ul>



<h2 class="wp-block-heading">Arguments that don&#8217;t survive contact</h2>



<ul class="wp-block-list">
<li><strong>&#8220;A VPS is faster because you get dedicated resources.&#8221;</strong> Only if it&#8217;s configured well. A default LEMP install with no object cache and an untuned PHP-FPM pool loses to a managed platform&#8217;s edge cache every time.</li>

<li><strong>&#8220;Managed hosting handles security, so I&#8217;m covered.&#8221;</strong> It handles infrastructure security. The most common route into a WordPress site is a vulnerable plugin, and that&#8217;s yours in both models.</li>

<li><strong>&#8220;I&#8217;ll just move if I outgrow it.&#8221;</strong> Migration cost is real and grows with the site. Redirect rules, caching behaviour, deployment workflow and email configuration all need rebuilding.</li>

<li><strong>&#8220;The VPS is only a few dollars a month.&#8221;</strong> The instance is. Offsite backup storage, monitoring and your own hours are not, and they&#8217;re most of the number.</li>

<li><strong>&#8220;Managed hosts back everything up, so I don&#8217;t need my own backups.&#8221;</strong> Retention windows are finite and account access can be lost. Keep an independent copy the host doesn&#8217;t control. This applies to both models.</li>
</ul>



<h2 class="wp-block-heading">Frequently asked questions</h2>



<h3 class="wp-block-heading">Is a VPS actually cheaper than managed WordPress hosting?</h3>



<p class="wp-block-paragraph">For one site, usually not once you price your own time honestly. For several sites, usually yes, and the gap widens with each site you add. The crossover sits somewhere around three to five sites for most people, depending on what your hours are worth and how much the sites change.</p>



<h3 class="wp-block-heading">Why does my managed host report more visitors than Google Analytics?</h3>



<p class="wp-block-paragraph">They measure different things. Analytics counts browser sessions that executed its JavaScript, so it misses anything blocking scripts and misses non-browser clients entirely. Your host counts requests reaching the server, deduplicated by IP per day. Bot filtering closes some of the gap but never all of it. The two numbers are not supposed to match.</p>



<h3 class="wp-block-heading">Can I run other applications alongside WordPress on managed hosting?</h3>



<p class="wp-block-paragraph">Generally no. Managed WordPress platforms run WordPress and nothing else. A background worker, a separate API, a queue or a self-hosted analytics instance needs a VPS or a second platform. Factor that second bill into the comparison.</p>



<h3 class="wp-block-heading">What happens if I install a disallowed plugin?</h3>



<p class="wp-block-paragraph">It depends on the host, but the documented pattern at WP Engine is that periodic filesystem scans detect it, you get notified, and the plugin is removed. Check the specific host&#8217;s current policy and enforcement mechanism before migrating, and check it against your live plugin list rather than what you assume is installed.</p>



<h3 class="wp-block-heading">Do I still need a caching plugin on a VPS?</h3>



<p class="wp-block-paragraph">You need caching. Whether it&#8217;s a plugin is a design choice. The strongest setup is full-page caching at the web server or CDN layer plus a persistent object cache in Redis or Memcached, which handles the repeated database queries page caching can&#8217;t touch. A page cache alone leaves logged-in traffic and admin requests hitting the database on every load.</p>



<h3 class="wp-block-heading">Is managed VPS hosting a real middle ground or just marketing?</h3>



<p class="wp-block-paragraph">It&#8217;s real, but the scope varies enormously and the word isn&#8217;t standardised. Ask specifically: who applies OS security patches, who restarts services afterwards, who owns backups and where they live, whether restores are tested, and whether the WordPress stack is in scope or the coverage stops at the operating system. Get the answers in writing before comparing prices.</p>



<h3 class="wp-block-heading">How hard is migrating from managed hosting to a VPS?</h3>



<p class="wp-block-paragraph">Moving files and the database is the easy part. The work is everything the platform did invisibly: redirect rules living in the host&#8217;s dashboard, cache purging behaviour, transactional email, TLS renewal, and any deployment workflow tied to the platform. Plan a parallel run with low DNS TTLs rather than a cutover, and keep the old plan alive until the new one has survived a full traffic cycle.</p>



<h2 class="wp-block-heading">The one thing worth remembering</h2>



<p class="wp-block-paragraph">Managed WordPress hosting vs VPS isn&#8217;t a performance question, and it isn&#8217;t really a price question either. It&#8217;s a question about which failures you&#8217;re equipped to own.</p>



<p class="wp-block-paragraph">Managed hosting converts operational risk into a predictable bill and a set of constraints you have to live inside. A VPS converts that bill into flexibility plus a standing obligation to do the boring work: patch, restart, and prove your restores actually restore. Both are defensible. What isn&#8217;t defensible is buying a VPS because it looked cheap and then skipping the work, which is the most common failure in this whole category and the one that ends with a rebuild from a backup nobody ever tested.</p>



<p class="wp-block-paragraph">Run the restore drill. Whatever you choose.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Need a second opinion on your hosting decision?</h2>



<p class="wp-block-paragraph">Most of the hosting work I get asked for is one of these. If it looks like your situation, I can help:</p>



<ul class="wp-block-list">
<li><strong>Cost modelling before you commit.</strong> Working out from your own access logs what a visit-metered plan would actually bill you, against a realistically priced VPS including backup storage and hours.</li>

<li><strong>Migration in either direction.</strong> Managed to VPS or VPS to managed, planned as a parallel run with DNS TTL staging so there&#8217;s no blind cutover.</li>

<li><strong>Building the VPS properly the first time.</strong> Nginx or Apache with PHP-FPM sized to the actual RAM, Redis object cache, TLS with automated renewal, firewall and Fail2ban, log rotation that doesn&#8217;t fill the disk.</li>

<li><strong>Backups you&#8217;ve actually restored.</strong> Offsite destination, retention policy, and a scheduled restore drill that fails loudly instead of silently.</li>

<li><strong>Plugin policy audits.</strong> Checking your live plugin list against a specific host&#8217;s disallowed list before migration, and finding replacements for anything that fails.</li>

<li><strong>Monitoring that answers the right question.</strong> Grafana and Prometheus dashboards covering PHP-FPM saturation, database load and cache hit ratio, not just whether the site returns 200.</li>
</ul>



<p class="wp-block-paragraph">Send me something concrete and I&#8217;ll tell you what I see: an access log sample, your current plan and traffic numbers, your active plugin list, or the output of the commands above. That&#8217;s usually enough for a straight answer before either of us talks about scope.</p>



<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="https://www.upwork.com/freelancers/~01f15a912ad84a6620" target="_blank" rel="noreferrer noopener">Work with me on Upwork</a></div>
</div>
<p>The post <a href="https://john-nessime.com/blog/wordpress/managed-wordpress-hosting-vs-vps/">Managed WordPress Hosting vs VPS: Who Owns the Failure?</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>The Cluster Becomes the Product: When Kubernetes Is Overkill</title>
		<link>https://john-nessime.com/blog/devops/when-kubernetes-is-overkill/</link>
		
		<dc:creator><![CDATA[John Nessime]]></dc:creator>
		<pubDate>Mon, 03 Aug 2026 15:26:00 +0000</pubDate>
				<category><![CDATA[Cloud Computing]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[Containers]]></category>
		<category><![CDATA[Deployment]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[Docker Compose]]></category>
		<category><![CDATA[Infrastructure]]></category>
		<category><![CDATA[Platform Engineering]]></category>
		<category><![CDATA[Production]]></category>
		<category><![CDATA[Self Hosting]]></category>
		<category><![CDATA[SRE]]></category>
		<category><![CDATA[VPS]]></category>
		<category><![CDATA[Web Hosting]]></category>
		<guid isPermaLink="false">https://john-nessime.com/blog/?p=93</guid>

					<description><![CDATA[<p>Three hours into an ingress controller upgrade for an application that is one Go binary and a Postgres database. Nothing is broken. The cluster has just become the thing you operate. An honest look at what Kubernetes costs, when it earns that cost, and what to run instead.</p>
<p>The post <a href="https://john-nessime.com/blog/devops/when-kubernetes-is-overkill/">The Cluster Becomes the Product: When Kubernetes Is Overkill</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">It&#8217;s a Tuesday and you&#8217;re three hours into an ingress controller upgrade. The CRD schema changed, the annotations you copied from a blog post two years ago are deprecated, and cert-manager wants a newer API version than the one your Helm chart pins.</p>



<p class="wp-block-paragraph">The application behind all of this is one Go binary and a Postgres database. It serves a few hundred requests a minute. Nobody has touched its code in a fortnight.</p>



<p class="wp-block-paragraph">That is the moment worth noticing: <strong>when Kubernetes is overkill</strong>, the tell is not that anything is broken. Everything works. It is that the cluster has quietly become the thing you operate, and the product has become a tenant.</p>



<h2 class="wp-block-heading">What it actually buys you</h2>



<p class="wp-block-paragraph">Worth being fair, because the case for Kubernetes is real and I reach for it regularly.</p>



<p class="wp-block-paragraph">It gives you bin-packing across many workloads, so a fleet of services shares machines efficiently. It gives you a declarative API that a platform team can build on, so twelve product teams deploy without twelve bespoke pipelines. It gives you self-healing, rolling updates, horizontal autoscaling and namespace-level isolation as defaults rather than as things you script. And it gives you a portable vocabulary: an engineer who knows Kubernetes knows most of your infrastructure on day one.</p>



<p class="wp-block-paragraph">Every one of those benefits scales with the number of services and the number of teams. None of them scale with how modern you want to feel.</p>



<h2 class="wp-block-heading">The bill nobody itemises</h2>



<p class="wp-block-paragraph">The cost is not the control plane fee. It is the surface area you have agreed to keep current.</p>



<p class="wp-block-paragraph">Kubernetes ships three minor releases a year and supports each for about fourteen months, and you can skip at most one minor version when upgrading. So there is a permanent, non-negotiable upgrade cadence: roughly every four months something needs planning, testing and a maintenance window, forever. Fall behind and you are doing multi-hop upgrades on an unsupported version, which is the worst combination available.</p>



<p class="wp-block-paragraph">Underneath that sit the components nobody counts when they say &#8220;we&#8217;ll just run Kubernetes&#8221;: a CNI plugin, a CSI driver, an ingress controller, cert-manager, external-dns, a metrics pipeline, an autoscaler, RBAC, and whatever operators your databases need. Each has its own release cycle, its own breaking changes and its own opinions about the others. That is where the Tuesday goes.</p>



<p class="wp-block-paragraph">Then there is debugging. A request that used to fail in one place now fails in one of several, and telling apart an application bug from a NetworkPolicy, a readiness probe, a DNS caching issue or a node under memory pressure is a genuine skill. On a good team that skill exists. On a three-person team it exists in one person&#8217;s head and goes on holiday with them.</p>



<h2 class="wp-block-heading">Signals you don&#8217;t need it yet</h2>



<ul class="wp-block-list">
<li>You have fewer than about five services, and most of them are one binary and a database.</li>
<li>One team owns everything, so there is nothing to isolate from anyone.</li>
<li>Traffic is predictable enough that you size for peak and stop thinking about it.</li>
<li>Nobody on the team has run a cluster in production before.</li>
<li>Your deploys are already fine. Nobody is complaining about them.</li>
<li>You cannot name the specific problem Kubernetes is solving, only the general one.</li>
<li>The cluster would run on two nodes, which means you have a scheduler for a decision with two possible answers.</li>
</ul>



<h2 class="wp-block-heading">Signals you probably do</h2>



<ul class="wp-block-list">
<li>Multiple teams deploying independently and tripping over each other&#8217;s environments.</li>
<li>Enough services that per-service deployment scripts have become their own maintenance problem.</li>
<li>Genuinely variable load where autoscaling saves real money, not hypothetical money.</li>
<li>A compliance or tenancy requirement that maps cleanly onto namespaces and network policy.</li>
<li>You are already running an operator-based product, like a database platform, that expects Kubernetes.</li>
<li>Somebody&#8217;s whole job is the platform, and it is not also somebody&#8217;s whole job to ship features.</li>
</ul>



<p class="wp-block-paragraph">That last one is the honest test. Kubernetes is not expensive to install. It is expensive to own, and ownership needs a name attached to it.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">What I&#8217;d reach for instead</h2>



<p class="wp-block-paragraph">Roughly in order of how much you take on.</p>



<p class="wp-block-paragraph"><strong>One server, systemd, and a reverse proxy.</strong> Underrated to the point of being contrarian, and completely adequate for a large number of real businesses. A VPS from Hetzner, InterServer or DigitalOcean, nginx or Caddy in front, systemd units for your services, automated backups off the box. Restart policies and health checks are built in; you are not going without them, you are just getting them from init instead of from a control loop.</p>



<p class="wp-block-paragraph"><strong>Docker Compose on one box.</strong> The step up when you want containers without a scheduler. The entire deployment is a file you can read in one screen:</p>



<pre class="wp-block-code"><code>services:
  app:
    image: ghcr.io/acme/api:1.4.2      # pinned, deployed by changing this line
    restart: always
    ports: ["127.0.0.1:8080:8080"]     # proxy handles TLS and the public port
    env_file: .env
    depends_on: [db]

  db:
    image: postgres:16                 # pin the major, upgrade deliberately
    restart: always
    volumes: ["pgdata:/var/lib/postgresql/data"]

volumes:
  pgdata:</code></pre>



<p class="wp-block-paragraph">The honest limitation: one box is one box. Losing it is an outage, and a redeploy is a brief gap unless you put something in front. For plenty of internal tools and early products, that is an acceptable trade stated out loud rather than an oversight.</p>



<p class="wp-block-paragraph"><strong>A platform that runs containers for you.</strong> Cloud Run, App Runner, Fly.io, Render, Railway. You hand over a container image and get scaling, TLS, rolling deploys and health checks without operating any of it. This is the option most teams should look at hardest, because it buys back the majority of what Kubernetes offers at almost none of the operational cost. You pay in per-unit pricing and in reduced control, and both are usually worth it below a certain scale.</p>



<p class="wp-block-paragraph"><strong>ECS on Fargate, or Nomad.</strong> Real schedulers with a fraction of the concepts. If you need orchestration but not the ecosystem, these are legitimate destinations rather than waypoints on the road to Kubernetes.</p>



<p class="wp-block-paragraph">And if you have decided Kubernetes genuinely is the answer, use a managed control plane, and consider a lightweight distribution such as k3s for small or edge deployments. Do not build the cluster yourself to save money. That is the most expensive saving in infrastructure.</p>



<h2 class="wp-block-heading">Arguments that don&#8217;t survive contact</h2>



<p class="wp-block-paragraph"><em>&#8220;We&#8217;ll need it eventually.&#8221;</em> Possibly. Adopting it three years early costs three years of upgrade cycles to buy an option you may not exercise, and the migration is not much harder later than it is now.</p>



<p class="wp-block-paragraph"><em>&#8220;It&#8217;s the industry standard.&#8221;</em> For orchestrating fleets, yes. Most applications are not fleets. Standard does not mean universally appropriate, and the same argument would put a load balancer in front of a single server.</p>



<p class="wp-block-paragraph"><em>&#8220;It avoids vendor lock-in.&#8221;</em> Partly true and frequently oversold. You have swapped a cloud provider&#8217;s API for a distribution, a set of controllers, and a pile of YAML that encodes assumptions about all of them. Portability lives in your container images and your data, not in the scheduler.</p>



<p class="wp-block-paragraph"><em>&#8220;It&#8217;ll help us hire.&#8221;</em> It helps you hire people who want to run Kubernetes. Whether that is the same as the people who will build your product is worth a moment&#8217;s thought.</p>



<h2 class="wp-block-heading">You&#8217;re not painting yourself into a corner</h2>



<p class="wp-block-paragraph">This is the part that makes the decision easy to defer, and it is the most useful thing in this post.</p>



<p class="wp-block-paragraph">The durable investments are containerising your application, externalising configuration into environment variables, keeping state out of the application and in a database or object storage, exposing a health endpoint, and logging to stdout. Do those and your app already satisfies most of what a Kubernetes manifest asks for. The scheduler underneath is genuinely swappable.</p>



<p class="wp-block-paragraph">So the choice is not Kubernetes now or a painful rewrite later. It is Kubernetes now, or the same containers running somewhere simpler until the complexity is earned.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">FAQ</h2>



<h3 class="wp-block-heading">How many services before Kubernetes makes sense?</h3>



<p class="wp-block-paragraph">There is no clean number, and anyone offering one is guessing. The better question is how many teams deploy independently. One team can coordinate without a platform. Four cannot, and that is when the abstraction starts paying for itself.</p>



<h3 class="wp-block-heading">Is Docker Compose really production-ready?</h3>



<p class="wp-block-paragraph">On a single host, for a workload that tolerates brief restarts, yes. It runs plenty of real systems. What it does not give you is failover across machines, so be clear that losing the host means an outage and decide whether that is acceptable rather than discovering it.</p>



<h3 class="wp-block-heading">Doesn&#8217;t managed Kubernetes remove the operational burden?</h3>



<p class="wp-block-paragraph">It removes the control plane, which is the part that was never your main problem. Ingress, storage, networking, upgrades and the add-on ecosystem all remain yours. Managed offerings genuinely help, and they narrow the gap rather than closing it.</p>



<h3 class="wp-block-heading">What about autoscaling? I can&#8217;t get that elsewhere.</h3>



<p class="wp-block-paragraph">You can. Cloud Run, App Runner, Fly and ECS all autoscale, several of them to zero, with no cluster to maintain. Autoscaling is a common reason to reach for Kubernetes and one of the weakest, because it is the feature most thoroughly commoditised elsewhere.</p>



<h3 class="wp-block-heading">How hard is it to migrate later?</h3>



<p class="wp-block-paragraph">Much easier than migrating off it. If your app is containerised, stateless, configured by environment and observable, moving to Kubernetes is mostly writing manifests. The hard parts of that migration are the ones you should be fixing anyway.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">The one thing to remember</h2>



<p class="wp-block-paragraph">Kubernetes is not complicated by accident. It is complicated because it solves a genuinely complicated problem, and if you do not have that problem you are paying the complexity and collecting none of the benefit.</p>



<p class="wp-block-paragraph">So make it a decision with a reason attached. Write down the specific thing you cannot do today, and check whether a cluster is the cheapest way to fix it. Sometimes it is. Often the honest answer is one server, a reverse proxy, and an afternoon spent on backups instead.</p>



<h2 class="wp-block-heading">Trying to decide?</h2>



<p class="wp-block-paragraph">I get called in on both sides of this: teams drowning in a cluster they did not need, and teams who genuinely need one and are putting it off. Work I take on:</p>



<ul class="wp-block-list">
<li>A straight assessment of whether Kubernetes fits your team and workload, with the reasoning written down rather than an opinion delivered.</li>
<li>Building the simpler thing properly: single-host or Compose deployments with TLS, health checks, deploys and backups that hold up in production.</li>
<li>Moving off an over-provisioned cluster onto something proportionate, without downtime.</li>
<li>Getting an application genuinely portable so the decision stays reversible: containerised, stateless, configured by environment, observable.</li>
<li>Setting up managed Kubernetes properly when it is the right answer, including the upgrade process nobody plans for.</li>
<li>Reviewing the cost, in money and in engineering hours, of what you currently run.</li>
</ul>



<p class="wp-block-paragraph">Tell me how many services you run and how many people deploy them, and I will tell you what I would build.</p>



<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="https://www.upwork.com/freelancers/~01f15a912ad84a6620" target="_blank" rel="noreferrer noopener">Work with me on Upwork</a></div>
</div>
<p>The post <a href="https://john-nessime.com/blog/devops/when-kubernetes-is-overkill/">The Cluster Becomes the Product: When Kubernetes Is Overkill</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>DNS Troubleshooting: How to Find the Real Cause in Minutes, Not Hours</title>
		<link>https://john-nessime.com/blog/networking/dns-troubleshooting/</link>
		
		<dc:creator><![CDATA[John Nessime]]></dc:creator>
		<pubDate>Thu, 30 Jul 2026 13:50:39 +0000</pubDate>
				<category><![CDATA[DevOps]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Networking]]></category>
		<category><![CDATA[System Administration]]></category>
		<category><![CDATA[Web Hosting]]></category>
		<category><![CDATA[BIND]]></category>
		<category><![CDATA[Cloudflare]]></category>
		<category><![CDATA[dig]]></category>
		<category><![CDATA[DNS]]></category>
		<category><![CDATA[DNS Cache]]></category>
		<category><![CDATA[DNS Records]]></category>
		<category><![CDATA[DNS Troubleshooting]]></category>
		<category><![CDATA[DNSSEC]]></category>
		<category><![CDATA[Domain Configuration]]></category>
		<category><![CDATA[Linux Networking]]></category>
		<category><![CDATA[MX Records]]></category>
		<category><![CDATA[Nameservers]]></category>
		<category><![CDATA[Network Debugging]]></category>
		<category><![CDATA[NSlookup]]></category>
		<category><![CDATA[NXDOMAIN]]></category>
		<category><![CDATA[SERVFAIL]]></category>
		<category><![CDATA[SPF]]></category>
		<category><![CDATA[Sysadmin]]></category>
		<category><![CDATA[TTL]]></category>
		<guid isPermaLink="false">https://john-nessime.com/blog/?p=27</guid>

					<description><![CDATA[<p>Most "the site is down" tickets are really DNS problems wearing a costume. Here's the practical DNS troubleshooting workflow I use — how to read dig output, follow the delegation chain, decode SERVFAIL and NXDOMAIN, and stop blaming propagation for things that are actually caching, TTL, or DNSSEC failures.</p>
<p>The post <a href="https://john-nessime.com/blog/networking/dns-troubleshooting/">DNS Troubleshooting: How to Find the Real Cause in Minutes, Not Hours</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></description>
										<content:encoded><![CDATA[

<p class="wp-block-paragraph">Someone messages you: &#8220;the site is down.&#8221; You open it — it loads fine on your machine. You ask them to try again, and it still fails. Nothing changed on the server. Nothing changed in the code. And somewhere in the back of your head a small voice says: <em>it&#8217;s DNS again.</em></p>



<p class="wp-block-paragraph">It usually is. Not because DNS is fragile, but because it&#8217;s the one layer where caching, delegation, and propagation all conspire to make the same domain behave differently depending on who&#8217;s asking and when. A record can be perfectly correct in your control panel and still resolve to the wrong IP for half the internet.</p>



<p class="wp-block-paragraph">This article is the DNS troubleshooting workflow I actually use — the order of checks, the commands worth memorising, how to read what they tell you, and the specific failure patterns that show up over and over. No theory dumps. Just the path from &#8220;something&#8217;s broken&#8221; to &#8220;here&#8217;s exactly which record on which server is wrong.&#8221;</p>



<h2 class="wp-block-heading">First, Get the Mental Model Right</h2>



<p class="wp-block-paragraph">You can&#8217;t debug a system you don&#8217;t have a picture of. When a browser resolves <code>example.com</code>, several independent parties are involved, and any one of them can be the liar:</p>



<ol class="wp-block-list"><li><strong>The application</strong> — browsers keep their own short-lived DNS cache, and HSTS or connection pooling can mask changes entirely.</li><li><strong>The operating system</strong> — the local stub resolver, <code>/etc/hosts</code>, <code>nscd</code>, <code>systemd-resolved</code>, or the Windows DNS Client service.</li><li><strong>The recursive resolver</strong> — your router, your ISP, or a public resolver like 1.1.1.1 or 8.8.8.8. This is where most stale answers live.</li><li><strong>The delegation chain</strong> — root servers, then the TLD servers, then whichever name servers your registrar says are authoritative.</li><li><strong>The authoritative name server</strong> — the only place where the truth actually lives.</li></ol>



<p class="wp-block-paragraph">Effective DNS troubleshooting is just working down that list and asking each party the same question directly, instead of trusting whatever the browser tells you. The moment you find two parties giving different answers, you&#8217;ve found your boundary — and the problem is between them.</p>



<h2 class="wp-block-heading">The Tools That Matter</h2>



<p class="wp-block-paragraph">You need surprisingly few. My honest ranking:</p>



<h3 class="wp-block-heading">dig — the one you should learn properly</h3>



<p class="wp-block-paragraph"><code>dig</code> is the only tool that shows you everything: the query it sent, the response status, the TTL remaining, the authority section, and whether the answer came from a cache or from the source. On Debian and Ubuntu it lives in <code>dnsutils</code>; on RHEL-family systems it&#8217;s in <code>bind-utils</code>.</p>



<pre class="wp-block-code"><code># Just the answer, nothing else
dig +short example.com A

# The full picture — status, flags, TTL, authority
dig example.com A

# Ask one specific resolver instead of the system default
dig @1.1.1.1 example.com A

# Ask a specific authoritative server directly
dig @ns1.example-dns.com example.com A</code></pre>



<h3 class="wp-block-heading">host — for quick sanity checks</h3>



<pre class="wp-block-code"><code>host example.com
host -t MX example.com
host -t TXT example.com</code></pre>



<h3 class="wp-block-heading">nslookup — fine, but know its limits</h3>



<p class="wp-block-paragraph"><code>nslookup</code> is everywhere, including Windows, which is its main virtue. But it hides details you often need, and its &#8220;Non-authoritative answer&#8221; banner confuses people constantly — it simply means the answer came from a cache rather than the authoritative server. That&#8217;s completely normal. On Windows I reach for PowerShell instead:</p>



<pre class="wp-block-code"><code>Resolve-DnsName example.com -Type A
Resolve-DnsName example.com -Server 1.1.1.1</code></pre>



<h3 class="wp-block-heading">resolvectl — essential on modern Linux</h3>



<p class="wp-block-paragraph">If the machine runs <code>systemd-resolved</code>, your <code>/etc/resolv.conf</code> probably points at <code>127.0.0.53</code>, which tells you nothing about the real upstream servers. <code>resolvectl</code> does:</p>



<pre class="wp-block-code"><code>resolvectl status
resolvectl query example.com
resolvectl statistics</code></pre>



<h3 class="wp-block-heading">getent — the reality check</h3>



<p class="wp-block-paragraph"><code>dig</code> talks to DNS. Your applications talk to the C library resolver, which also consults <code>/etc/hosts</code> and whatever else <code>/etc/nsswitch.conf</code> lists. When <code>dig</code> and the application disagree, this is the command that explains why:</p>



<pre class="wp-block-code"><code>getent hosts example.com
getent ahosts example.com</code></pre>



<h2 class="wp-block-heading">A Repeatable DNS Troubleshooting Workflow</h2>



<p class="wp-block-paragraph">This is the sequence. Don&#8217;t skip ahead — each step eliminates a whole class of causes, and jumping straight to the authoritative server is how you spend an hour fixing something that was never broken.</p>



<h3 class="wp-block-heading">Step 1 — Confirm it&#8217;s actually DNS</h3>



<p class="wp-block-paragraph">Resolve the name, then connect to the resulting IP directly. If the IP works and the name doesn&#8217;t, it&#8217;s DNS. If neither works, you&#8217;re troubleshooting the wrong layer.</p>



<pre class="wp-block-code"><code>dig +short example.com A
curl -I --resolve example.com:443:203.0.113.10 https://example.com/</code></pre>



<p class="wp-block-paragraph">The <code>--resolve</code> flag is underrated. It forces curl to use an IP you specify while still sending the correct Host header and SNI, which lets you test the web server as if DNS were already fixed. Replace <code>203.0.113.10</code> with the IP you expect.</p>



<h3 class="wp-block-heading">Step 2 — Compare local vs public resolvers</h3>



<pre class="wp-block-code"><code>dig +short example.com A
dig +short @1.1.1.1 example.com A
dig +short @8.8.8.8 example.com A
dig +short @9.9.9.9 example.com A</code></pre>



<p class="wp-block-paragraph">If your local answer differs from all three public resolvers, the problem is on your side — a cache, a hosts file entry, a VPN pushing internal DNS, or a router doing something creative. If all four agree but the answer is wrong, the problem is upstream at the authoritative layer.</p>



<h3 class="wp-block-heading">Step 3 — Find out who is actually authoritative</h3>



<p class="wp-block-paragraph">This is the step people skip, and it&#8217;s the one that catches the nastiest problems. The name servers listed in your DNS provider&#8217;s dashboard are not necessarily the ones the internet is using.</p>



<pre class="wp-block-code"><code># What the parent zone (the registry) says
dig +short NS example.com

# What the zone itself says, straight from one of those servers
dig @ns1.example-dns.com example.com NS</code></pre>



<p class="wp-block-paragraph">Those two lists should match. When they don&#8217;t, you&#8217;ve usually found a half-finished DNS migration: the registrar still delegates to the old provider, so the world keeps reading a zone nobody has updated in months.</p>



<h3 class="wp-block-heading">Step 4 — Query every authoritative server individually</h3>



<p class="wp-block-paragraph">A zone with four name servers can be inconsistent across them if a zone transfer failed. Intermittent, &#8220;it works every third time&#8221; problems are almost always this. Compare the SOA serial on each:</p>



<pre class="wp-block-code"><code># Pull the SOA record from every authoritative server in one shot
dig +nssearch example.com

# Or check them one by one
dig +short @ns1.example-dns.com example.com SOA
dig +short @ns2.example-dns.com example.com SOA</code></pre>



<p class="wp-block-paragraph">Different serial numbers mean the secondaries haven&#8217;t caught up with the primary. Different answers for the same record mean something is genuinely broken in replication.</p>



<h3 class="wp-block-heading">Step 5 — Walk the delegation chain</h3>



<p class="wp-block-paragraph">When nothing else explains it, follow the query the way a recursive resolver would — from the root, down through the TLD, to the authoritative servers:</p>



<pre class="wp-block-code"><code>dig +trace example.com A</code></pre>



<p class="wp-block-paragraph">Read it top to bottom. Each block is one hop. The point where the output stops making sense — a referral to servers that don&#8217;t answer, a missing glue record, an unexpected NXDOMAIN — is your break point. This single command has saved me more time than any other in DNS troubleshooting.</p>



<h2 class="wp-block-heading">Reading dig Output Without Guessing</h2>



<p class="wp-block-paragraph">The status line in a <code>dig</code> response tells you the category of failure immediately. Learn these four and you&#8217;ve eliminated most of the guesswork.</p>



<ul class="wp-block-list"><li><strong>NOERROR</strong> — the query succeeded. Note that NOERROR with an empty answer section is <em>not</em> a failure: it means the name exists but has no record of the type you asked for. Asking for an A record on a name that only has MX records gives you exactly this.</li><li><strong>NXDOMAIN</strong> — the name does not exist at all. Not a typo in the record; the label itself isn&#8217;t in the zone. Check for a missing record, a misspelled hostname, or a zone that isn&#8217;t loaded.</li><li><strong>SERVFAIL</strong> — the resolver tried and failed. This is the ambiguous one, and in practice it usually means DNSSEC validation failure, an authoritative server that&#8217;s unreachable or refusing queries, or a broken delegation.</li><li><strong>REFUSED</strong> — the server you asked is not willing to answer for that zone. Typically you&#8217;re querying a server that isn&#8217;t authoritative and doesn&#8217;t do recursion for you.</li></ul>



<p class="wp-block-paragraph">Two other things worth reading in every response:</p>



<ul class="wp-block-list"><li><strong>The <code>aa</code> flag</strong> — present when the answer came from an authoritative server. If it&#8217;s missing, you&#8217;re looking at a cached answer, and the TTL tells you how stale it might be.</li><li><strong>The TTL column</strong> — on a cached answer this counts <em>down</em>. Query the same resolver twice a few seconds apart; if the TTL drops, you&#8217;re being served from cache. If it stays constant, you&#8217;re hitting the source.</li></ul>




<h2 class="wp-block-heading">The Propagation Myth (and What&#8217;s Really Happening)</h2>



<p class="wp-block-paragraph">&#8220;DNS propagation takes 24 to 48 hours&#8221; is the most repeated and least accurate sentence in web hosting. Nothing propagates. When you change a record, the authoritative server has the new value instantly. What takes time is the expiry of cached copies held by thousands of independent resolvers — and that duration is entirely determined by the TTL you set <em>before</em> the change.</p>



<p class="wp-block-paragraph">The practical consequence: lowering a TTL after you&#8217;ve made the change does nothing for anyone who already cached the old value. Plan ahead instead.</p>



<ol class="wp-block-list"><li>Well before a migration, drop the TTL on the records you&#8217;ll change to something short — 300 seconds is a common choice.</li><li>Wait at least the length of the <em>old</em> TTL so the short value fully replaces it in caches.</li><li>Make the actual change. Old cached answers now expire within minutes.</li><li>Verify against several public resolvers and from a couple of different networks.</li><li>Once everything is stable, raise the TTL back to something sane — an hour or more — so you&#8217;re not paying for lookups you don&#8217;t need.</li></ol>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">The TTL you set today determines how painful next month&#8217;s migration will be. It&#8217;s the cheapest piece of planning in infrastructure work, and the one most consistently skipped.</p>
</blockquote>



<p class="wp-block-paragraph">One more thing people miss: <strong>negative caching</strong>. When a lookup returns NXDOMAIN, resolvers cache that &#8220;doesn&#8217;t exist&#8221; answer too, for a duration derived from the minimum field in the zone&#8217;s SOA record. So if you query a hostname <em>before</em> creating it, you may keep getting NXDOMAIN for a while even after the record is live. Check what you&#8217;re in for:</p>



<pre class="wp-block-code"><code>dig +short example.com SOA</code></pre>



<p class="wp-block-paragraph">The last number in that output is the negative caching TTL. Resist the urge to test a hostname before you&#8217;ve actually created it.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Failure Patterns You&#8217;ll Meet Again and Again</h2>



<h3 class="wp-block-heading">A CNAME on the zone apex</h3>



<p class="wp-block-paragraph">You cannot put a plain CNAME on the root of a domain. The apex must carry SOA and NS records, and a CNAME is not allowed to coexist with other records at the same name. Providers work around this with ALIAS, ANAME, or &#8220;CNAME flattening&#8221; records that resolve the target server-side and return an A record. If your root domain misbehaves while <code>www</code> is fine, this is the first thing to check.</p>



<h3 class="wp-block-heading">Stale cache — everywhere at once</h3>



<p class="wp-block-paragraph">Correct at the authoritative server, wrong on your machine. Clear caches from the outside in:</p>



<pre class="wp-block-code"><code># Linux with systemd-resolved
sudo resolvectl flush-caches

# Linux with nscd
sudo systemctl restart nscd

# macOS
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder

# Windows (run as Administrator)
ipconfig /flushdns</code></pre>



<p class="wp-block-paragraph">Then remember the browser. Chromium-based browsers keep a separate cache you can clear at <code>chrome://net-internals/#dns</code>. And if the site uses HSTS, a browser may refuse to fall back to HTTP regardless of what DNS says — test with <code>curl</code> before you conclude anything from a browser.</p>



<h3 class="wp-block-heading">A forgotten line in /etc/hosts</h3>



<p class="wp-block-paragraph">Someone adds a hosts entry during a migration to test the new server, and never removes it. Six months later the server is &#8220;randomly&#8221; hitting the wrong backend. It costs three seconds to rule out:</p>



<pre class="wp-block-code"><code>grep -i example.com /etc/hosts
getent hosts example.com</code></pre>



<h3 class="wp-block-heading">DNSSEC validation failures</h3>



<p class="wp-block-paragraph">A domain with broken DNSSEC returns SERVFAIL from validating resolvers while working perfectly on non-validating ones. That produces the maddening symptom where the site loads for some users and not others, with no geographic pattern.</p>



<p class="wp-block-paragraph">The diagnostic trick is <code>+cd</code>, which disables validation. If the query fails normally but succeeds with <code>+cd</code>, DNSSEC is your culprit:</p>



<pre class="wp-block-code"><code>dig @1.1.1.1 example.com A
dig @1.1.1.1 +cd example.com A

# Show the DNSSEC records themselves
dig example.com DNSKEY +dnssec
dig example.com DS

# delv performs validation and explains the outcome
delv example.com A</code></pre>



<p class="wp-block-paragraph">The usual cause is a DS record at the registrar that no longer matches the keys published in the zone — classic after changing DNS providers without disabling DNSSEC first. <strong>Always turn DNSSEC off at the registrar before migrating name servers, then re-enable it once the new provider is live and stable.</strong></p>



<h3 class="wp-block-heading">Split-horizon DNS surprises</h3>



<p class="wp-block-paragraph">Internal resolvers hand out private addresses; external resolvers hand out public ones. Perfectly intentional — until a VPN, a container, or a misconfigured search domain puts a client on the wrong side of the split. When a name resolves differently depending on <em>where</em> you run the query, compare the local resolver against a public one and check whether a VPN profile is pushing its own DNS servers.</p>



<h3 class="wp-block-heading">Proxied records hiding the origin</h3>



<p class="wp-block-paragraph">If a domain sits behind Cloudflare or a similar proxy, public DNS returns the proxy&#8217;s IP, not your server&#8217;s. That&#8217;s the whole point — but it means you cannot verify an origin change through public DNS at all. Query the authoritative provider&#8217;s own API or dashboard for the real record value, and test the origin directly with <code>curl --resolve</code>.</p>



<h3 class="wp-block-heading">Wildcards masking a missing record</h3>



<p class="wp-block-paragraph">A <code>*</code> record in the zone means every hostname resolves — including the ones you typo&#8217;d, and including subdomains you thought you&#8217;d deleted. If everything resolves but half of it points somewhere unexpected, check for a wildcard before you go hunting for phantom records.</p>



<h3 class="wp-block-heading">Missing trailing dots in zone files</h3>



<p class="wp-block-paragraph">Hand-editing BIND zone files? A record value without a trailing dot gets the origin appended to it. Write <code>mail.example.com</code> instead of <code>mail.example.com.</code> and you&#8217;ve just created <code>mail.example.com.example.com</code>. Validate before reloading:</p>



<pre class="wp-block-code"><code>named-checkconf
named-checkzone example.com /var/named/example.com.zone</code></pre>



<p class="wp-block-paragraph">And bump the SOA serial. A zone edit without a serial increment won&#8217;t reach the secondaries, which is exactly the inconsistency you were chasing in Step 4.</p>



<h3 class="wp-block-heading">Containers and the ndots trap</h3>



<p class="wp-block-paragraph">Inside Kubernetes pods, <code>/etc/resolv.conf</code> commonly sets <code>ndots:5</code>. Any name with fewer than five dots gets tried against every search domain first, so an external lookup like <code>api.example.com</code> can generate several failed queries before the correct one. It works — it&#8217;s just slow, and under load it looks like intermittent DNS failure. Adding a trailing dot to fully qualify the name skips the search list entirely:</p>



<pre class="wp-block-code"><code>cat /etc/resolv.conf
dig api.example.com.        # note the trailing dot</code></pre>



<h2 class="wp-block-heading">DNS Troubleshooting for Email Delivery</h2>



<p class="wp-block-paragraph">Mail problems are DNS problems more often than they are mail-server problems. When messages bounce or land in spam, check these before touching the mail server config:</p>



<pre class="wp-block-code"><code># Where should mail for this domain go?
dig +short example.com MX

# SPF and DMARC live in TXT records
dig +short example.com TXT
dig +short _dmarc.example.com TXT

# DKIM lives under a selector you choose
dig +short selector1._domainkey.example.com TXT

# Reverse DNS for the sending IP
dig -x 203.0.113.10 +short</code></pre>



<p class="wp-block-paragraph">Things that regularly go wrong here:</p>



<ul class="wp-block-list"><li><strong>Two SPF records.</strong> A domain must publish exactly one. Two SPF TXT records is a permanent error, and receiving servers are entitled to fail the check outright. Merge them into one.</li><li><strong>An MX pointing at a CNAME.</strong> MX targets must resolve to address records, not aliases.</li><li><strong>Missing reverse DNS.</strong> If the PTR record for your sending IP doesn&#8217;t match the hostname the server announces, expect a lot of spam folder.</li><li><strong>DKIM selector mismatch.</strong> The selector in the message header must match the one published in DNS — a detail that quietly breaks after a mail platform migration.</li></ul>



<p class="wp-block-paragraph">While you&#8217;re in TXT territory: if certificate issuance is failing, check for a CAA record. A CAA entry that only authorises one certificate authority will silently block every other one, and the error you get back from the ACME client rarely says &#8220;DNS.&#8221;</p>



<pre class="wp-block-code"><code>dig +short example.com CAA</code></pre>



<h2 class="wp-block-heading">Mistakes That Cost the Most Time</h2>



<ul class="wp-block-list"><li><strong>Testing only in a browser.</strong> Browser caches, HSTS, service workers, and QUIC connection reuse will all lie to you. Confirm with <code>dig</code> and <code>curl</code>.</li><li><strong>Trusting the control panel.</strong> The dashboard shows what the zone <em>should</em> contain. Only a query against the authoritative server shows what it actually serves.</li><li><strong>Changing several things at once.</strong> Adjust one record, verify, then move on. Batch changes turn a five-minute fix into an archaeology project.</li><li><strong>Forgetting IPv6.</strong> A stale or wrong AAAA record produces &#8220;works for some people&#8221; behaviour, because dual-stack clients generally prefer IPv6. Always check <code>dig +short example.com AAAA</code>.</li><li><strong>Leaving DNSSEC enabled through a provider migration.</strong> The single most reliable way to take a domain fully offline for hours.</li><li><strong>Not documenting the zone before a migration.</strong> Export or record every record first. Rebuilding a zone from memory at 2am is not a plan.</li></ul>



<h2 class="wp-block-heading">Best Practices That Prevent the Next Incident</h2>



<ul class="wp-block-list"><li><strong>Use at least two name servers</strong>, ideally on separate networks or providers. Single-provider DNS is a single point of failure for your entire presence.</li><li><strong>Keep TTLs deliberate.</strong> Long TTLs for stable infrastructure, short ones ahead of planned changes. Not the other way round.</li><li><strong>Monitor DNS resolution itself</strong>, not just HTTP endpoints. A check that queries your authoritative servers directly and alerts on a changed or missing answer catches problems before users do.</li><li><strong>Version-control your zone data</strong> if your provider supports API or zone-file export. Diffing two zone versions turns &#8220;what changed?&#8221; into a one-second question.</li><li><strong>Keep a record inventory</strong> — what each record is for, who owns it, and whether it&#8217;s still needed. Old A records pointing at decommissioned servers are a genuine security risk, not just clutter.</li><li><strong>Standardise your checks.</strong> A short script that runs your Step 1–5 sequence against a domain makes DNS troubleshooting repeatable instead of improvised.</li></ul>



<h2 class="wp-block-heading">A Quick Reference You Can Keep</h2>



<pre class="wp-block-code"><code># Basic resolution
dig +short example.com A
dig +short example.com AAAA

# Compare resolvers
dig +short @1.1.1.1 example.com A
dig +short @8.8.8.8 example.com A

# Authority and delegation
dig +short example.com NS
dig +trace example.com A
dig +nssearch example.com

# Cache behaviour
dig example.com A            # watch the TTL count down
dig +norecurse @1.1.1.1 example.com A   # cached-only lookup

# DNSSEC
dig +cd example.com A
delv example.com A

# Mail and verification records
dig +short example.com MX
dig +short example.com TXT
dig +short _dmarc.example.com TXT
dig -x 203.0.113.10 +short

# System-level view
resolvectl status
getent hosts example.com
cat /etc/resolv.conf</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Frequently Asked Questions</h2>



<h3 class="wp-block-heading">How long does a DNS change really take to take effect?</h3>



<p class="wp-block-paragraph">At the authoritative server, immediately. For everyone else, up to the TTL that was in place when they last cached the record. If the old TTL was 3600 seconds, worst case is roughly an hour. The &#8220;24 to 48 hours&#8221; figure is a legacy of very long default TTLs and badly behaved resolvers, not a rule.</p>



<h3 class="wp-block-heading">What&#8217;s the difference between SERVFAIL and NXDOMAIN?</h3>



<p class="wp-block-paragraph">NXDOMAIN is a definitive answer: the name doesn&#8217;t exist. SERVFAIL means the resolver couldn&#8217;t produce an answer at all — commonly a DNSSEC validation failure, an unreachable authoritative server, or a broken delegation. NXDOMAIN points you at the zone contents; SERVFAIL points you at the infrastructure.</p>



<h3 class="wp-block-heading">Why does the site work for me but not for a colleague?</h3>



<p class="wp-block-paragraph">Different resolvers with different cache states, a hosts file entry on one machine, a VPN pushing internal DNS, an IPv6 versus IPv4 difference, or DNSSEC failing only on validating resolvers. Have both people run the same <code>dig</code> against the same public resolver — if the answers match, the difference is local to one machine.</p>



<h3 class="wp-block-heading">Should I use dig or nslookup?</h3>



<p class="wp-block-paragraph"><code>dig</code>, whenever it&#8217;s available. It shows response status, flags, TTLs and the authority section, all of which you need for real DNS troubleshooting. Use <code>nslookup</code> or <code>Resolve-DnsName</code> when you&#8217;re on a Windows box without other tooling.</p>



<h3 class="wp-block-heading">Do online &#8220;DNS propagation checker&#8221; sites actually help?</h3>



<p class="wp-block-paragraph">They&#8217;re useful for one thing: seeing what different resolvers around the world currently have cached. They won&#8217;t tell you <em>why</em> an answer is wrong, and they can&#8217;t see proxied origins or internal split-horizon zones. Treat them as a rough sanity check, not a diagnosis.</p>



<h3 class="wp-block-heading">Can a CNAME point to another CNAME?</h3>



<p class="wp-block-paragraph">Technically yes, and resolvers will follow the chain — but every extra hop adds latency and another thing to break. Keep chains short, and never let one loop back on itself. Also remember a CNAME cannot coexist with other records at the same name, which is why it doesn&#8217;t belong on a zone apex.</p>



<h3 class="wp-block-heading">Why do my DNS changes keep reverting?</h3>



<p class="wp-block-paragraph">Usually because something else owns the zone: an infrastructure-as-code pipeline, a control panel that rewrites records on certain actions, or an external DNS controller in a Kubernetes cluster. Decide on one source of truth and make every change through it.</p>



<h3 class="wp-block-heading">Is switching to a public resolver a fix?</h3>



<p class="wp-block-paragraph">It&#8217;s a workaround for one user, not a fix for the domain. It&#8217;s genuinely useful as a diagnostic — if 1.1.1.1 gives the right answer and your ISP&#8217;s resolver doesn&#8217;t, you&#8217;ve isolated the problem to that resolver&#8217;s cache. But if the record itself is wrong, changing resolvers just changes who tells you the wrong thing.</p>



<h2 class="wp-block-heading">Wrapping Up</h2>



<p class="wp-block-paragraph">Good DNS troubleshooting isn&#8217;t about knowing obscure record types. It&#8217;s about discipline: confirm it&#8217;s DNS, compare resolvers, find out who&#8217;s really authoritative, ask each of them directly, and follow the delegation chain when the answers stop agreeing. Five steps, in order, every time.</p>



<p class="wp-block-paragraph">Do that consistently and most incidents collapse into something small and obvious — a stale cache, a TTL nobody lowered, a DS record left behind after a migration, a hosts entry from six months ago. The problems aren&#8217;t usually exotic. They&#8217;re just invisible until you ask the right party the right question.</p>



<p class="wp-block-paragraph">Keep the quick-reference commands somewhere you can reach in thirty seconds, set your TTLs before you need them, and monitor resolution as seriously as you monitor uptime. Future you will appreciate it at 2am.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Need a Hand With DNS, Hosting or Cloud Infrastructure?</h2>



<p class="wp-block-paragraph">If you&#8217;re stuck on a DNS issue, planning a migration you&#8217;d rather not do twice, or you just want someone to review a zone before you flip the switch, I&#8217;m available for freelance work.</p>



<p class="wp-block-paragraph">I work on DNS configuration and troubleshooting, domain and hosting migrations, SSL and certificate issues, email deliverability records (SPF, DKIM, DMARC), server setup and hardening, AWS and cloud infrastructure, and monitoring and alerting — plus WordPress performance and maintenance.</p>



<p class="wp-block-paragraph">You can see my profile, past work and reviews on Upwork, and message me directly there with what you&#8217;re dealing with.</p>



<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="https://www.upwork.com/freelancers/~01f15a912ad84a6620" target="_blank" rel="noreferrer noopener">Hire Me on Upwork</a></div>
</div>



<p class="wp-block-paragraph">Got a DNS problem this article didn&#8217;t cover? Leave a comment describing the symptoms and what <code>dig +trace</code> shows — that output alone usually narrows it down fast.</p>

<!-- /wp:post-content --><p>The post <a href="https://john-nessime.com/blog/networking/dns-troubleshooting/">DNS Troubleshooting: How to Find the Real Cause in Minutes, Not Hours</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>WordPress Migration Without Downtime: A Practical Guide to Moving Your Website Safely</title>
		<link>https://john-nessime.com/blog/wordpress/wordpress-migration-without-downtime/</link>
		
		<dc:creator><![CDATA[John Nessime]]></dc:creator>
		<pubDate>Thu, 16 Jul 2026 20:32:36 +0000</pubDate>
				<category><![CDATA[Hosting & Infrastructure]]></category>
		<category><![CDATA[Technical Guides]]></category>
		<category><![CDATA[Website Management]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[DNS Migration]]></category>
		<category><![CDATA[SSL Certificate]]></category>
		<category><![CDATA[Web Hosting]]></category>
		<category><![CDATA[Website Downtime]]></category>
		<category><![CDATA[Website Migration]]></category>
		<category><![CDATA[Website Performance]]></category>
		<category><![CDATA[WordPress Backup]]></category>
		<category><![CDATA[WordPress Database]]></category>
		<category><![CDATA[WordPress Hosting]]></category>
		<category><![CDATA[WordPress Migration]]></category>
		<guid isPermaLink="false">https://john-nessime.com/blog/?p=23</guid>

					<description><![CDATA[<p>Moving a WordPress website involves much more than copying files. This practical migration guide explains how to transfer WordPress safely, minimize downtime, protect SEO, preserve email services, and avoid lost data.</p>
<p>The post <a href="https://john-nessime.com/blog/wordpress/wordpress-migration-without-downtime/">WordPress Migration Without Downtime: A Practical Guide to Moving Your Website Safely</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></description>
										<content:encoded><![CDATA[


<p class="wp-block-paragraph">Moving a WordPress website sounds simple until the site contains years of content, customer data, business emails, analytics integrations, payment systems, and search-engine rankings that cannot afford to disappear.</p>




<p class="wp-block-paragraph">A successful WordPress migration is not just a matter of copying files from one server to another. It is a controlled infrastructure change involving the website files, database, domain records, SSL certificate, email configuration, caching layers, scheduled tasks, security settings, and third-party integrations.</p>




<p class="wp-block-paragraph">When the process is planned correctly, visitors may never notice that the website has moved. When it is rushed, the result can include broken pages, missing images, database errors, lost form submissions, email disruption, redirect loops, certificate warnings, or several hours of downtime.</p>




<p class="wp-block-paragraph">This guide explains how to migrate a WordPress website safely, how to reduce downtime, what can go wrong, and what should be verified before the old hosting environment is finally switched off.</p>




<h2 class="wp-block-heading">What Is a WordPress Migration?</h2>




<p class="wp-block-paragraph">A WordPress migration is the process of moving a WordPress website from one environment to another. The destination might be a new hosting provider, a cloud server, a virtual private server, a staging environment, a new domain, or a different server configuration.</p>




<p class="wp-block-paragraph">The website may look like a single application from the visitor’s perspective, but several separate components normally need to be transferred:</p>




<ul class="wp-block-list">
<li>WordPress core files</li>
<li>Theme files</li>
<li>Plugin files</li>
<li>Uploaded media</li>
<li>The MySQL or MariaDB database</li>
<li>Configuration files</li>
<li>Custom code and server rules</li>
<li>DNS records</li>
<li>SSL certificates</li>
<li>Email-related DNS records</li>
<li>Cron jobs and scheduled tasks</li>
<li>CDN and caching configuration</li>
<li>Analytics and tracking scripts</li>
<li>External API integrations</li>
</ul>




<p class="wp-block-paragraph">A complete migration must preserve both the visible website and the underlying functionality. Copying the homepage successfully is not enough if contact forms stop sending, checkout sessions fail, redirects break, or administrators can no longer log in.</p>




<h2 class="wp-block-heading">Common Reasons for Migrating a WordPress Website</h2>




<p class="wp-block-paragraph">Website owners migrate WordPress installations for many different reasons. In most cases, the decision is connected to performance, reliability, cost, control, or security.</p>




<h3 class="wp-block-heading">Slow Hosting Performance</h3>




<p class="wp-block-paragraph">A website may outgrow its original shared hosting plan. Pages become slow, the WordPress dashboard takes longer to load, and traffic spikes begin causing resource-limit errors. Moving to a better-managed host, VPS, or cloud server can provide more CPU, memory, storage performance, and configuration flexibility.</p>




<h3 class="wp-block-heading">Frequent Downtime</h3>




<p class="wp-block-paragraph">Unreliable infrastructure can damage customer trust and search visibility. If the current host experiences repeated outages or provides weak technical support, migration may be the most practical long-term solution.</p>




<h3 class="wp-block-heading">Security Concerns</h3>




<p class="wp-block-paragraph">Outdated server software, poor account isolation, weak backup systems, or limited firewall controls can expose a WordPress website to unnecessary risk. A migration can be used as an opportunity to rebuild the site on a cleaner and more secure environment.</p>




<h3 class="wp-block-heading">Hosting Cost Optimization</h3>




<p class="wp-block-paragraph">Some hosting plans become expensive as traffic grows, especially when backups, staging, CDN access, email, malware scanning, or additional storage are charged separately. Migrating to a more suitable platform can reduce costs without sacrificing performance.</p>




<h3 class="wp-block-heading">Business Growth</h3>




<p class="wp-block-paragraph">A small brochure website may eventually become a membership platform, online store, booking portal, or content-heavy publication. The original environment may no longer support the website’s operational requirements.</p>




<h3 class="wp-block-heading">Domain or Branding Changes</h3>




<p class="wp-block-paragraph">Some migrations involve changing the domain name as well as the server. This requires additional planning because internal URLs, redirects, analytics properties, search-engine records, cookies, and third-party callbacks may all depend on the original domain.</p>




<h2 class="wp-block-heading">The Difference Between a Simple Migration and a Production Migration</h2>




<p class="wp-block-paragraph">A small personal blog with few updates can often be moved using a migration plugin. A production website requires more care.</p>




<p class="wp-block-paragraph">Production websites may receive new orders, registrations, comments, bookings, support requests, or form submissions while the migration is taking place. If the database is copied too early and DNS is updated hours later, any data created during that gap may remain only on the old server.</p>




<p class="wp-block-paragraph">That is why a production migration should be treated as a controlled cutover rather than a one-click file transfer. The objective is to minimize the period during which the old and new environments contain different information.</p>




<h2 class="wp-block-heading">What Can Go Wrong During a WordPress Migration?</h2>




<p class="wp-block-paragraph">Most migration problems fall into a predictable set of categories. Knowing them in advance makes the process easier to plan and troubleshoot.</p>




<h3 class="wp-block-heading">Database Connection Errors</h3>




<p class="wp-block-paragraph">The WordPress configuration file may contain the wrong database name, username, password, or database host. The user may also lack the necessary permissions on the destination database.</p>




<h3 class="wp-block-heading">Broken Internal URLs</h3>




<p class="wp-block-paragraph">WordPress stores URLs in the database. If the domain, protocol, folder path, or staging address changes, old references can remain inside posts, menus, widgets, page-builder content, serialized options, and plugin settings.</p>




<h3 class="wp-block-heading">Missing Images or Media Files</h3>




<p class="wp-block-paragraph">The uploads directory may be incomplete, excluded from the transfer, or copied with incorrect ownership and permissions. A site may appear functional while older images silently return 404 errors.</p>




<h3 class="wp-block-heading">PHP Compatibility Problems</h3>




<p class="wp-block-paragraph">The destination server may use a newer PHP version than the original host. An outdated theme or plugin can then generate warnings, fatal errors, blank pages, or broken administrative screens.</p>




<h3 class="wp-block-heading">Redirect Loops</h3>




<p class="wp-block-paragraph">Conflicting HTTPS, proxy, CDN, and WordPress redirect rules can cause the browser to repeatedly redirect between two versions of the same URL.</p>




<h3 class="wp-block-heading">Email Delivery Failure</h3>




<p class="wp-block-paragraph">A migration may affect outgoing form emails, SMTP authentication, domain verification, or mailbox routing. DNS changes can also unintentionally overwrite MX, SPF, DKIM, or DMARC records.</p>




<h3 class="wp-block-heading">SSL Certificate Warnings</h3>




<p class="wp-block-paragraph">The destination server may not yet have a valid certificate, or the certificate may not cover every hostname used by the website. Mixed-content warnings can also appear when pages load images, scripts, or stylesheets through HTTP.</p>




<h3 class="wp-block-heading">Lost Transactions or Form Submissions</h3>




<p class="wp-block-paragraph">This is one of the most serious risks. If visitors continue using the old website after the final database backup, their activity may not exist on the new server.</p>




<h2 class="wp-block-heading">Phase One: Audit the Existing Website</h2>




<p class="wp-block-paragraph">Before copying anything, document the current environment. A migration is much safer when the source system is fully understood.</p>




<p class="wp-block-paragraph">The audit should include:</p>




<ul class="wp-block-list">
<li>Current WordPress version</li>
<li>PHP version and PHP extensions</li>
<li>Database engine and version</li>
<li>Active theme and child theme</li>
<li>Active and inactive plugins</li>
<li>Disk usage</li>
<li>Database size</li>
<li>Upload directory size</li>
<li>Web server type</li>
<li>Caching configuration</li>
<li>Scheduled cron tasks</li>
<li>CDN or reverse proxy usage</li>
<li>External SMTP provider</li>
<li>Payment and booking integrations</li>
<li>DNS provider</li>
<li>SSL configuration</li>
<li>Backup availability</li>
</ul>




<p class="wp-block-paragraph">This audit often reveals problems that already existed before the migration. For example, a plugin may be abandoned, the database may contain excessive transient records, or the site may still rely on an unsupported PHP version.</p>




<p class="wp-block-paragraph">A migration should not automatically copy every old problem into the new environment. It is often the right time to clean up unused plugins, remove abandoned themes, review administrator accounts, and confirm that all essential components remain supported.</p>




<h2 class="wp-block-heading">Phase Two: Prepare the Destination Server</h2>




<p class="wp-block-paragraph">The destination environment should be ready before the website is transferred. This reduces last-minute troubleshooting during the cutover.</p>




<p class="wp-block-paragraph">The server preparation usually includes:</p>




<ul class="wp-block-list">
<li>Creating the hosting account or virtual host</li>
<li>Installing the correct PHP version</li>
<li>Enabling required PHP extensions</li>
<li>Creating the database and database user</li>
<li>Configuring file permissions</li>
<li>Setting PHP memory and upload limits</li>
<li>Configuring the web server</li>
<li>Preparing SSL issuance</li>
<li>Configuring backups</li>
<li>Installing monitoring or logging tools</li>
<li>Preparing caching and performance settings</li>
</ul>




<p class="wp-block-paragraph">The destination should ideally match the source environment closely during the first transfer. Major software upgrades can be performed later, after the website is stable. Combining a hosting migration, PHP upgrade, plugin cleanup, theme redesign, and domain change into one maintenance window increases the number of possible failure points.</p>




<h2 class="wp-block-heading">Phase Three: Create Verified Backups</h2>




<p class="wp-block-paragraph">A migration should never begin without a complete backup. More importantly, the backup should be verified rather than merely assumed to be usable.</p>




<p class="wp-block-paragraph">At minimum, create separate copies of:</p>




<ul class="wp-block-list">
<li>The entire WordPress file system</li>
<li>The website database</li>
<li>The WordPress configuration file</li>
<li>Web server rules</li>
<li>DNS records</li>
<li>SSL-related information</li>
<li>Custom cron jobs</li>
<li>Email routing records</li>
</ul>




<p class="wp-block-paragraph">A backup is only valuable if it can be restored. Confirm that the archive opens correctly, the database export is not empty or truncated, and the files include the full uploads directory.</p>




<p class="wp-block-paragraph">For business-critical websites, keeping more than one backup copy is sensible. One copy may remain on the source server, while another is stored externally.</p>




<h2 class="wp-block-heading">Phase Four: Copy the WordPress Files</h2>




<p class="wp-block-paragraph">The files can be transferred using several methods, including a migration plugin, hosting control panel, SFTP, SCP, rsync, or a compressed archive.</p>




<p class="wp-block-paragraph">For small websites, a plugin may be sufficient. Larger websites often benefit from server-level transfer tools because they handle large file sets more efficiently and provide better visibility into transfer errors.</p>




<p class="wp-block-paragraph">During the transfer, confirm that hidden files are included. Files such as <code>.htaccess</code> may contain essential rewrite, security, redirect, or caching rules.</p>




<p class="wp-block-paragraph">After copying, verify ownership and permissions. Incorrect permissions can prevent uploads, plugin updates, cache creation, or WordPress core operations.</p>




<h2 class="wp-block-heading">Phase Five: Export and Import the Database</h2>




<p class="wp-block-paragraph">The database contains the website’s posts, pages, users, settings, menus, plugin data, WooCommerce orders, form entries, and most application-level configuration.</p>




<p class="wp-block-paragraph">It can be exported using tools such as phpMyAdmin, WP-CLI, a migration plugin, or the database command line. Large databases are generally more reliable to move through command-line tools because browser-based imports may fail due to upload or execution limits.</p>




<p class="wp-block-paragraph">Once imported, update the destination <code>wp-config.php</code> file with the correct database details:</p>




<pre class="wp-block-code"><code>define('DB_NAME', 'destination_database');
define('DB_USER', 'destination_user');
define('DB_PASSWORD', 'strong_password');
define('DB_HOST', 'localhost');</code></pre>





<p class="wp-block-paragraph">The exact database host depends on the hosting environment. Some providers use a remote database hostname instead of <code>localhost</code>.</p>




<h2 class="wp-block-heading">Phase Six: Handle URL Changes Correctly</h2>




<p class="wp-block-paragraph">If the website keeps the same domain, fewer URL changes may be required. However, staging URLs, temporary hostnames, HTTP-to-HTTPS changes, subdirectory changes, or domain changes require a controlled search-and-replace operation.</p>




<p class="wp-block-paragraph">A plain text replacement inside a database export can damage serialized WordPress data. WordPress plugins and page builders often store structured values where string lengths matter.</p>




<p class="wp-block-paragraph">Use a WordPress-aware tool such as WP-CLI:</p>




<pre class="wp-block-code"><code>wp search-replace 'https://old-domain.example' 'https://new-domain.example' --all-tables --precise --dry-run</code></pre>





<p class="wp-block-paragraph">The dry run displays the expected replacements without modifying the database. Once reviewed, the operation can be repeated without the <code>--dry-run</code> flag.</p>




<p class="wp-block-paragraph">After the replacement, check:</p>




<ul class="wp-block-list">
<li>Page and post links</li>
<li>Navigation menus</li>
<li>Image URLs</li>
<li>Page-builder templates</li>
<li>Widgets</li>
<li>Logo and favicon paths</li>
<li>Canonical tags</li>
<li>Open Graph images</li>
<li>Plugin callback URLs</li>
<li>Webhook endpoints</li>
</ul>




<h2 class="wp-block-heading">Phase Seven: Test the Website Before Changing DNS</h2>




<p class="wp-block-paragraph">The destination website should be tested before public traffic is sent to it. This is one of the most important steps in a low-downtime migration.</p>




<p class="wp-block-paragraph">A local hosts-file override can map the production domain to the new server only on your own computer. This allows the website to be tested under its real domain while the rest of the internet continues reaching the old server.</p>




<p class="wp-block-paragraph">Testing should cover more than the homepage.</p>




<h3 class="wp-block-heading">Front-End Testing</h3>




<ul class="wp-block-list">
<li>Homepage</li>
<li>Posts and pages</li>
<li>Category and archive pages</li>
<li>Search functionality</li>
<li>Navigation menus</li>
<li>Images and downloads</li>
<li>Responsive layout</li>
<li>404 page</li>
<li>Redirects</li>
</ul>




<h3 class="wp-block-heading">Administrative Testing</h3>




<ul class="wp-block-list">
<li>WordPress login</li>
<li>Post editing</li>
<li>Media uploads</li>
<li>Plugin settings</li>
<li>Theme customization</li>
<li>User management</li>
<li>Scheduled tasks</li>
</ul>




<h3 class="wp-block-heading">Business Function Testing</h3>




<ul class="wp-block-list">
<li>Contact forms</li>
<li>Newsletter subscriptions</li>
<li>Account registration</li>
<li>Password reset emails</li>
<li>Checkout and payment flow</li>
<li>Booking forms</li>
<li>Customer dashboards</li>
<li>API integrations</li>
<li>Webhook delivery</li>
</ul>




<p class="wp-block-paragraph">Check the browser console, server logs, WordPress debug logs, and network requests. A page can appear normal while scripts, API calls, or background processes are failing silently.</p>




<h2 class="wp-block-heading">How to Reduce WordPress Migration Downtime</h2>




<p class="wp-block-paragraph">Zero downtime is difficult to guarantee for every website, but downtime can often be reduced to a very small window.</p>




<h3 class="wp-block-heading">Lower the DNS TTL in Advance</h3>




<p class="wp-block-paragraph">DNS Time to Live determines how long resolvers may cache a record. Lowering the TTL before migration can help changes propagate more quickly.</p>




<p class="wp-block-paragraph">This should be done before the migration window, not at the exact moment of cutover. Existing cached records will continue using the previous TTL until they expire.</p>




<h3 class="wp-block-heading">Perform an Initial Copy Before Cutover</h3>




<p class="wp-block-paragraph">Copy the website files and database to the destination in advance. This gives enough time to test the environment and resolve compatibility problems before the final switch.</p>




<h3 class="wp-block-heading">Freeze Content Changes Briefly</h3>




<p class="wp-block-paragraph">For dynamic websites, a short maintenance or content-freeze window may be necessary. During this period, editors should stop publishing and customers may need to be temporarily prevented from submitting transactions.</p>




<h3 class="wp-block-heading">Perform a Final Database Synchronization</h3>




<p class="wp-block-paragraph">After the initial copy and testing, export the latest database immediately before the DNS or proxy cutover. This minimizes the chance of losing recent activity.</p>




<h3 class="wp-block-heading">Keep the Old Server Available</h3>




<p class="wp-block-paragraph">Do not cancel the original hosting account as soon as DNS is changed. Some visitors may still reach the old server due to DNS caching. The source environment should remain available until propagation is complete and the new website has been monitored.</p>




<h2 class="wp-block-heading">DNS Migration: More Than Changing One Record</h2>




<p class="wp-block-paragraph">DNS errors are responsible for many migration failures. A domain may have records for the website, email, verification services, subdomains, APIs, and third-party platforms.</p>




<p class="wp-block-paragraph">Before making changes, export or document the complete DNS zone. Important records may include:</p>




<ul class="wp-block-list">
<li>A records</li>
<li>AAAA records</li>
<li>CNAME records</li>
<li>MX records</li>
<li>TXT records</li>
<li>SPF records</li>
<li>DKIM records</li>
<li>DMARC records</li>
<li>Domain verification records</li>
<li>Service-specific subdomains</li>
</ul>




<p class="wp-block-paragraph">If only the web server is changing, there may be no reason to modify email records. Accidentally replacing the entire DNS zone with a simplified version can interrupt business email even when the website itself works correctly.</p>




<h2 class="wp-block-heading">SSL and HTTPS After Migration</h2>




<p class="wp-block-paragraph">The destination server must have a valid SSL certificate for the production domain. The certificate should cover all required hostnames, such as the root domain and the <code>www</code> version.</p>




<p class="wp-block-paragraph">After enabling HTTPS, verify that:</p>




<ul class="wp-block-list">
<li>The certificate is valid and not expired</li>
<li>The certificate matches the domain</li>
<li>HTTP redirects to HTTPS correctly</li>
<li>The root and www versions behave consistently</li>
<li>No redirect loop exists</li>
<li>Images and scripts load through HTTPS</li>
<li>CDN resources use secure URLs</li>
</ul>




<p class="wp-block-paragraph">Mixed-content errors should be corrected rather than hidden. They usually indicate that some database values, theme settings, CSS files, or external resources still reference HTTP URLs.</p>




<h2 class="wp-block-heading">Email and Contact Form Verification</h2>




<p class="wp-block-paragraph">A website migration is not complete until email functionality is tested.</p>




<p class="wp-block-paragraph">WordPress may rely on the local server mail function, SMTP authentication, a transactional email provider, or an external mailbox service. Each method has different requirements.</p>




<p class="wp-block-paragraph">Test at least the following:</p>




<ul class="wp-block-list">
<li>Contact form delivery</li>
<li>Administrator notifications</li>
<li>Password reset emails</li>
<li>WooCommerce order emails</li>
<li>Registration emails</li>
<li>Newsletter confirmation messages</li>
<li>SMTP authentication</li>
</ul>




<p class="wp-block-paragraph">Also confirm that SPF, DKIM, and DMARC records remain valid. Successful message submission does not always mean successful inbox delivery.</p>




<h2 class="wp-block-heading">Migrating a WooCommerce Website</h2>




<p class="wp-block-paragraph">WooCommerce migrations require additional care because the database may change continuously. Orders, inventory, customer records, carts, subscriptions, refunds, and payment events can be created during the migration window.</p>




<p class="wp-block-paragraph">A WooCommerce migration plan should include:</p>




<ul class="wp-block-list">
<li>A clearly defined cutover time</li>
<li>A short checkout freeze if required</li>
<li>A final database synchronization</li>
<li>Payment gateway testing</li>
<li>Webhook testing</li>
<li>Order email verification</li>
<li>Inventory validation</li>
<li>Subscription and scheduled-payment checks</li>
<li>Customer login testing</li>
<li>Transaction log review</li>
</ul>




<p class="wp-block-paragraph">For high-traffic stores, a simple export-and-import workflow may not be sufficient. More advanced database synchronization or application-level migration techniques may be needed.</p>




<h2 class="wp-block-heading">Migrating WordPress to a New Domain</h2>




<p class="wp-block-paragraph">Changing the domain adds an SEO and application layer to the migration.</p>




<p class="wp-block-paragraph">In addition to moving the website, the process should include:</p>




<ul class="wp-block-list">
<li>Updating WordPress URLs</li>
<li>Replacing old domain references</li>
<li>Creating page-to-page 301 redirects</li>
<li>Updating canonical URLs</li>
<li>Updating XML sitemaps</li>
<li>Updating analytics properties</li>
<li>Updating Search Console records</li>
<li>Updating social profiles</li>
<li>Updating payment callbacks</li>
<li>Updating API allowlists</li>
<li>Updating email addresses where required</li>
</ul>




<p class="wp-block-paragraph">A blanket redirect from every old URL to the new homepage is not a good migration strategy. Each important old URL should redirect to its closest equivalent on the new domain.</p>




<h2 class="wp-block-heading">Protecting SEO During WordPress Migration</h2>




<p class="wp-block-paragraph">A hosting migration that keeps the same domain should normally have limited SEO impact when implemented correctly. A domain change or large structural change requires more planning.</p>




<p class="wp-block-paragraph">Important SEO checks include:</p>




<ul class="wp-block-list">
<li>Preserving existing URL structures</li>
<li>Maintaining page titles and metadata</li>
<li>Keeping canonical tags correct</li>
<li>Preserving structured data</li>
<li>Checking robots.txt</li>
<li>Checking XML sitemaps</li>
<li>Verifying redirects</li>
<li>Removing accidental noindex directives</li>
<li>Checking HTTP status codes</li>
<li>Monitoring crawl errors</li>
<li>Monitoring organic traffic after launch</li>
</ul>




<p class="wp-block-paragraph">Staging environments are often configured with <code>noindex</code> rules to prevent search engines from indexing them. That setting must not be carried into production accidentally.</p>




<p class="wp-block-paragraph">It is also important to remove temporary password protection, development redirects, and staging-specific canonical URLs before launch.</p>




<h2 class="wp-block-heading">Performance Optimization After Migration</h2>




<p class="wp-block-paragraph">A migration provides a good opportunity to improve performance, but optimization should be performed methodically.</p>




<p class="wp-block-paragraph">Start by confirming that the website is stable. Then review:</p>




<ul class="wp-block-list">
<li>Page caching</li>
<li>Object caching</li>
<li>PHP OPcache</li>
<li>Database performance</li>
<li>Image compression</li>
<li>WebP or AVIF delivery</li>
<li>CDN integration</li>
<li>Browser caching</li>
<li>Gzip or Brotli compression</li>
<li>Slow plugins</li>
<li>Unused scripts and styles</li>
<li>External API delays</li>
</ul>




<p class="wp-block-paragraph">Do not enable several caching systems at the same time without understanding how they interact. Duplicate caching layers can create stale pages, login problems, checkout issues, and difficult-to-diagnose behavior.</p>




<h2 class="wp-block-heading">Security Checks After Migration</h2>




<p class="wp-block-paragraph">The new environment should be reviewed before the migration is considered complete.</p>




<p class="wp-block-paragraph">Post-migration security checks should include:</p>




<ul class="wp-block-list">
<li>Removing temporary migration files</li>
<li>Removing exported database archives</li>
<li>Deleting unused administrator accounts</li>
<li>Changing temporary credentials</li>
<li>Confirming file ownership and permissions</li>
<li>Disabling directory listing</li>
<li>Reviewing firewall rules</li>
<li>Enabling brute-force protection</li>
<li>Updating WordPress, themes, and plugins</li>
<li>Reviewing backup access</li>
<li>Protecting configuration files</li>
<li>Confirming malware scanning</li>
</ul>




<p class="wp-block-paragraph">Database exports and full-site archives often contain sensitive information. They should not remain publicly accessible inside the website directory after migration.</p>




<h2 class="wp-block-heading">WordPress Migration Plugins vs Manual Migration</h2>




<p class="wp-block-paragraph">Both methods are valid. The right choice depends on the website.</p>




<h3 class="wp-block-heading">Migration Plugins</h3>




<p class="wp-block-paragraph">Migration plugins are convenient for small and medium-sized websites. They can package files and databases, update URLs, and simplify restoration.</p>




<p class="wp-block-paragraph">However, they may encounter:</p>




<ul class="wp-block-list">
<li>File size limits</li>
<li>PHP execution time limits</li>
<li>Memory limits</li>
<li>Hosting restrictions</li>
<li>Archive extraction failures</li>
<li>Large database timeouts</li>
<li>Limited visibility into errors</li>
</ul>




<h3 class="wp-block-heading">Manual or Server-Level Migration</h3>




<p class="wp-block-paragraph">Manual migration provides more control and is often more reliable for large, customized, or business-critical websites.</p>




<p class="wp-block-paragraph">It also allows the migration specialist to inspect logs, control database operations, preserve permissions, perform incremental transfers, and troubleshoot individual layers separately.</p>




<p class="wp-block-paragraph">The tradeoff is that server-level migration requires stronger knowledge of Linux, databases, DNS, web servers, PHP, SSL, and WordPress internals.</p>




<h2 class="wp-block-heading">A Practical WordPress Migration Checklist</h2>




<h3 class="wp-block-heading">Before Migration</h3>




<ul class="wp-block-list">
<li>Audit the current website</li>
<li>Record PHP and database versions</li>
<li>List active integrations</li>
<li>Export DNS records</li>
<li>Lower DNS TTL if needed</li>
<li>Create and verify backups</li>
<li>Prepare the destination server</li>
<li>Install required PHP extensions</li>
<li>Create the destination database</li>
<li>Plan the maintenance window</li>
</ul>




<h3 class="wp-block-heading">During Migration</h3>




<ul class="wp-block-list">
<li>Copy WordPress files</li>
<li>Export and import the database</li>
<li>Update wp-config.php</li>
<li>Perform safe URL replacement</li>
<li>Verify permissions</li>
<li>Test using a hosts-file override</li>
<li>Check logs and browser errors</li>
<li>Test forms, logins, and transactions</li>
<li>Perform the final database sync</li>
<li>Update DNS</li>
</ul>




<h3 class="wp-block-heading">After Migration</h3>




<ul class="wp-block-list">
<li>Verify SSL</li>
<li>Test email delivery</li>
<li>Check redirects</li>
<li>Check sitemap and robots.txt</li>
<li>Clear all cache layers</li>
<li>Review application logs</li>
<li>Monitor uptime</li>
<li>Monitor search and analytics data</li>
<li>Keep the old server available temporarily</li>
<li>Remove migration archives</li>
<li>Confirm backup schedules</li>
</ul>




<h2 class="wp-block-heading">When Should You Hire a WordPress Migration Specialist?</h2>




<p class="wp-block-paragraph">A simple personal website may be moved successfully with a migration plugin. Professional assistance becomes more valuable when the website has operational or commercial importance.</p>




<p class="wp-block-paragraph">Consider hiring a specialist when:</p>




<ul class="wp-block-list">
<li>The website generates sales or leads</li>
<li>Downtime has a direct business cost</li>
<li>The database changes frequently</li>
<li>The site uses WooCommerce or memberships</li>
<li>The migration includes a domain change</li>
<li>Email services share the same DNS zone</li>
<li>The site contains custom plugins or code</li>
<li>The destination server requires manual configuration</li>
<li>The site has a large media library or database</li>
<li>You need SSL, DNS, redirects, and testing handled together</li>
</ul>




<p class="wp-block-paragraph">A professional migration should provide more than a copied website. It should include preparation, backups, testing, DNS coordination, SSL validation, post-migration verification, and a rollback plan.</p>




<h2 class="wp-block-heading">Final Thoughts</h2>




<p class="wp-block-paragraph">WordPress migration is a technical process, but the real objective is business continuity. Visitors should continue reaching the website, customers should continue completing transactions, administrators should retain access, and search engines should continue finding the same content.</p>




<p class="wp-block-paragraph">The safest migrations are rarely the fastest-looking ones. They are the migrations where the destination is prepared in advance, backups are verified, the website is tested before cutover, DNS is handled carefully, and the old environment remains available until the transition is confirmed.</p>




<p class="wp-block-paragraph">Whether the website is moving to a new hosting company, a VPS, a cloud server, or a new domain, a structured migration plan dramatically reduces the risk of downtime, missing data, and unexpected failures.</p>




<div class="wp-block-group" style="border-width:1px;border-radius:12px;margin-top:48px;padding-top:32px;padding-right:32px;padding-bottom:32px;padding-left:32px"><div class="wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-6f60fb98 wp-block-group-is-layout-constrained">



<h2 class="wp-block-heading has-text-align-center">Need Help Migrating Your WordPress Website?</h2>




<p class="has-text-align-center wp-block-paragraph">A WordPress migration should not put your website, customer data, email delivery, or search rankings at risk. I provide complete WordPress migration services covering website files, databases, DNS, SSL, testing, redirects, and post-migration verification.</p>




<p class="has-text-align-center wp-block-paragraph">Your website will be prepared, transferred, tested, and launched through a controlled migration process designed to minimize downtime and prevent avoidable disruption.</p>




<div class="wp-block-buttons is-content-justification-center is-layout-flex wp-container-core-buttons-is-layout-fe48e5de wp-block-buttons-is-layout-flex">



<div class="wp-block-button has-custom-width wp-block-button__width-75">
<a class="wp-block-button__link wp-element-button" href="https://www.upwork.com/services/product/development-it-wordpress-website-migration-with-minimal-downtime-2077830746744627334" target="_blank" rel="noopener sponsored" style="border-radius:8px;padding-top:14px;padding-right:24px;padding-bottom:14px;padding-left:24px">View My WordPress Migration Service on Upwork</a>
</div>


</div>


</div></div>


<p>The post <a href="https://john-nessime.com/blog/wordpress/wordpress-migration-without-downtime/">WordPress Migration Without Downtime: A Practical Guide to Moving Your Website Safely</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
