Category: Hosting

  • WordPress page builder bloat: what 3MB of inline CSS actually does

    WordPress page builder bloat: what 3MB of inline CSS actually does

    WordPress page builder bloat: what 3MB of inline CSS actually does
    Photo by Team Nocoloco on Unsplash

    Open the source code of a site built with Elementor, Divi, or WPBakery and you’ll often find 2,000–4,000 lines of CSS sitting inline in the <head>. Not linked. Not cached. Just dropped into the HTML on every single page load.

    That’s not a configuration mistake. It’s how most WordPress page builders work by default. And for solo operators running content sites, it’s one of the biggest hidden performance taxes you’re paying.

    What inline CSS actually costs

    A typical Elementor page with moderate styling generates around 250–400 KB of inline CSS. Divi can hit 500 KB. WPBakery sits somewhere in between. None of that is cached by the browser the way an external stylesheet would be.

    The performance hit shows up in three places:

    • Time to First Byte (TTFB): WordPress has to generate all that CSS server-side before it can send the page. On shared hosting or underpowered VPS instances, that adds 200–600ms.
    • Render blocking: Browsers can’t start painting the page until they’ve parsed all the CSS in the <head>. Inline styles delay First Contentful Paint (FCP) by forcing sequential processing.
    • Bandwidth waste: Every page view re-downloads styles that could have been cached once and reused across the entire site.

    Run a Lighthouse test on a page builder site and you’ll see the same warnings: “Reduce unused CSS,” “Eliminate render-blocking resources,” “Reduce server response times.” All symptoms of the same root cause.

    Why builders do this (and when it’s justified)

    Page builders inject inline CSS because it’s the only way to guarantee per-page design flexibility without managing a complex cascade of stylesheets. If every page can have unique column widths, custom font weights, and individual color overrides, bundling all possible styles into one external file doesn’t work—you’d ship megabytes of unused rules.

    For marketing sites, landing pages, or portfolio sites where design variety matters more than repeat-visitor performance, that trade-off makes sense. A freelancer showcasing client work doesn’t care if every project page regenerates its CSS—visitors rarely see more than one.

    But for content-driven businesses—blogs, newsletters, membership sites—the math flips. Your readers visit multiple pages per session. They come back weekly. And you’re competing on page speed in both Google’s Core Web Vitals rankings and reader patience.

    The fix: CSS print method and selective loading

    Most page builders have a setting buried in performance options called something like “CSS Print Method” or “CSS Loading Method.” The default is usually “Internal” or “Inline.” Switch it to “External” and the builder will write styles to a static file instead.

    In Elementor, it’s under Elementor → Settings → Features → Optimized CSS Loading. Enable it, clear your cache, and check the source again. You’ll see a <link> tag instead of a <style> block. That file gets cached by the browser and reused across every page visit.

    Divi has a similar toggle under Divi → Theme Options → Builder → Advanced → Static CSS File Generation. WPBakery requires a plugin or manual filter hook, but the principle is identical.

    One gotcha: external CSS files can break if your caching plugin and your page builder both try to manage them. If you enable external CSS and suddenly see unstyled pages, check for cache conflicts. Purge everything, disable asset optimization in your caching plugin temporarily, and re-test.

    When to skip the builder entirely

    If your site is 80% blog posts and 20% custom landing pages, you don’t need a page builder site-wide. Use the default block editor (Gutenberg) for posts and only activate the builder on pages where you need layout control.

    Most builders let you disable their CSS output on post types you’re not using. In Elementor, go to Settings → General and uncheck “Post” under “Post Types.” Your blog posts shed hundreds of kilobytes instantly.

    For operators running membership content, course platforms, or newsletter archives, this is the fastest performance win available. A plain-text post with a featured image and a couple of headings doesn’t need a visual builder. It needs fast delivery and readable typography.

    If you’re launching a new site in 2026 and content is the core product, start with a lightweight theme like GeneratePress or Kadence and only add a page builder if you hit a layout you can’t solve with blocks. You’ll save yourself months of performance firefighting later.

    Got a page builder performance question or a caching conflict you can’t solve? Hit reply—we’re collecting builder/host combinations that reliably break, and we’ll cover them in a future piece.

  • WordPress database query logs: how to read slow_query_log

    WordPress database query logs: how to read slow_query_log

    WordPress database query logs: how to read slow_query_log
    Photo: Will (Wiki Ed) via Wikimedia Commons (CC BY-SA 4.0)

    Most WordPress performance issues trace back to database queries. A plugin runs a suboptimal SELECT, your homepage load time jumps from 800ms to 4.2 seconds, and you’re left guessing which of your 23 active plugins is responsible.

    MySQL’s slow_query_log is the diagnostic tool that ends the guessing. It records every query that exceeds a time threshold you set—usually one or two seconds. The log tells you exactly which SQL statement ran, how long it took, and which rows it examined.

    Here’s how to turn it on, read the output, and use it to fix the query that’s killing your site speed.

    Enabling slow_query_log on your WordPress host

    Most managed WordPress hosts disable direct my.cnf access, but many expose slow query logging through their dashboard. BigScoots, for example, lets you toggle it on via cPanel’s MySQL configuration panel. If you’re on a VPS or dedicated server, you’ll edit the MySQL config file directly.

    Add these lines to /etc/mysql/my.cnf (or /etc/my.cnf depending on your distro):

    slow_query_log = 1
    slow_query_log_file = /var/log/mysql/slow-query.log
    long_query_time = 1

    The long_query_time value is in seconds. Setting it to 1 captures anything longer than one second. For high-traffic sites, you might start at 2 to reduce noise.

    Restart MySQL: sudo systemctl restart mysql

    Queries now log to /var/log/mysql/slow-query.log. If the file doesn’t exist, MySQL will create it on the first slow query.

    Reading the log: what the output actually means

    Open the log file. Each slow query entry looks like this:

    # Time: 2026-08-30T14:22:35.442891Z
    # User@Host: wpuser[wpuser] @ localhost []
    # Query_time: 3.204571 Lock_time: 0.000312 Rows_sent: 1847 Rows_examined: 124503
    SELECT * FROM wp_posts WHERE post_status = 'publish' ORDER BY post_date DESC;

    Here’s what matters:

    • Query_time: Total execution time in seconds. This query took 3.2 seconds.
    • Lock_time: Time waiting for table locks. Usually negligible unless you’re running MyISAM tables (you shouldn’t be).
    • Rows_sent: How many rows the query returned. Here, 1,847.
    • Rows_examined: How many rows MySQL scanned to build that result. Here, 124,503. That’s a 67:1 examination-to-return ratio—terrible efficiency.

    The query itself follows. In this case, it’s a broad SELECT * with no LIMIT clause, scanning every published post.

    Identifying the plugin or theme responsible

    The slow query log shows the SQL, but not which PHP file triggered it. To trace that, enable WordPress’s SAVEQUERIES constant in wp-config.php:

    define('SAVEQUERIES', true);

    Install the Query Monitor plugin. It cross-references the slow queries with the calling function, showing you the exact plugin or theme file responsible.

    In most cases, you’ll find one of three culprits:

    • A poorly-coded custom query in a theme’s functions.php
    • An analytics or “related posts” plugin running uncached lookups on every page load
    • A WooCommerce or membership plugin querying order or user meta without indexes

    Once you’ve identified the source, you have three options: optimize the query, cache the result, or replace the plugin.

    One non-obvious tip: check Rows_examined even for fast queries

    A query might finish in 0.8 seconds—just under your long_query_time threshold—but still examine 200,000 rows to return 12. That’s inefficient, and it will degrade as your database grows.

    Manually review your site’s most-used queries with Query Monitor’s “Queries by Component” view, sorted by row examination count. If any query examines more than 10x the rows it returns, add an index or rewrite it.

    Run EXPLAIN on the suspect query in phpMyAdmin or the MySQL command line to see which indexes MySQL is using (or ignoring). If the “type” column shows “ALL,” you’re doing a full table scan—add an index on the columns in your WHERE or ORDER BY clause.

    Want more infrastructure deep-dives like this? Subscribe to One Two Three Send—every article covers one specific tool, feature, or workflow decision for solo operators running content businesses.

  • WordPress lazy loading delays images differently on mobile vs desktop

    WordPress lazy loading delays images differently on mobile vs desktop

    WordPress lazy loading delays images differently on mobile vs desktop
    Photo by Stephen Phillips – Hostreviews.co.uk on Unsplash

    WordPress enabled native lazy loading by default in 2020, and most operators never think about it again. The browser handles it, images load as users scroll, and Core Web Vitals improve. Simple.

    Except lazy loading doesn’t work the same way on mobile and desktop. The threshold that triggers an image to start loading—called the “distance-from-viewport” threshold—varies by browser, device type, and connection speed. That inconsistency affects how fast your site feels, even when the technical metrics look fine.

    How lazy loading thresholds actually work

    When WordPress adds loading="lazy" to an image, it’s telling the browser: don’t load this until the user is about to see it. But “about to see it” isn’t a fixed distance.

    Chrome and Edge use a threshold of roughly 1250 pixels on desktop with a fast connection. That means an image starts loading when it’s still more than a full screen away. On mobile, that threshold drops to around 2500 pixels—proportionally much larger relative to viewport height, but still about two to three scroll lengths.

    On slower connections (defined by the browser’s effectiveType API), those thresholds shrink. Chrome drops to around 2500 pixels on desktop and 1250 pixels on mobile when it detects 3G speeds.

    Firefox uses similar logic but calculates thresholds as a multiplier of viewport height rather than fixed pixels. Safari’s implementation is more conservative across the board.

    The result: the same page can trigger image loads at wildly different scroll positions depending on device and connection. A hero image that loads instantly on desktop might not even start fetching until a mobile user scrolls halfway down the page.

    Where this breaks perceived performance

    Lazy loading improves Largest Contentful Paint (LCP) and Total Blocking Time by deferring offscreen images. But it can hurt perceived speed if critical above-the-fold images get lazy-loaded by mistake.

    WordPress excludes the first image in the content from lazy loading, but that heuristic fails when:

    • Your hero image is a background image in CSS, not an <img> tag
    • Your layout uses a sidebar, and the “first” image is a small thumbnail in the sidebar, not the main content image
    • You’re using a page builder that injects images via JavaScript after initial render

    On mobile, where thresholds are proportionally larger, users scroll faster and notice the delay more. An image that starts loading at 2500 pixels might not finish rendering before the user reaches it, especially on slower devices or connections.

    This is why some sites feel faster on desktop even when mobile scores higher in Lighthouse. The lazy loading threshold mismatch creates a perception gap that metrics don’t fully capture.

    How to adjust lazy loading behavior

    You can’t directly control browser thresholds, but you can control which images get the loading="lazy" attribute.

    The simplest fix: remove lazy loading from above-the-fold images. WordPress provides a filter to exclude specific images:

    add_filter( 'wp_lazy_loading_enabled', '__return_false' );

    That disables it globally, which is overkill. Instead, target specific images by hooking into wp_get_attachment_image_attributes and removing the loading attribute for images in the first few blocks or above a certain position in the DOM.

    Most page builders and themes offer a “disable lazy loading” toggle for hero sections. Use it. The performance cost of eagerly loading one or two large images is trivial compared to the perceived-speed hit of lazy-loading them incorrectly.

    For mobile-specific tuning, consider using the fetchpriority="high" attribute on your primary hero image. It’s supported in Chrome, Edge, and Safari, and it tells the browser to prioritize that image even if lazy loading would otherwise delay it. WordPress doesn’t add this by default, but you can inject it via the same attribute filter.

    Test with real throttling, not just Lighthouse

    Lighthouse simulates a slow connection, but it doesn’t show you the scroll-and-wait behavior real users experience. Open Chrome DevTools, switch to the Network tab, and throttle to “Slow 3G.” Then actually scroll your site on a mobile viewport.

    Watch where images start loading. If they’re popping in after you’ve scrolled past them, your thresholds are too conservative or your images are too large. If they’re all loading at once on page load, you’re not benefiting from lazy loading at all.

    Run the same test on desktop. The behavior should be noticeably different. If it’s not, you might have a plugin overriding WordPress’s native implementation with a JavaScript-based lazy loader—those usually use fixed thresholds regardless of device.

    Most solo operators optimize for Lighthouse scores and call it done. But perceived speed matters more than any single metric, and lazy loading thresholds are one of the few variables that affect perception without showing up in reports.

    Reply with the laziest-loading image you’ve ever encountered. We’re collecting examples for a follow-up piece on edge cases that break WordPress’s heuristics.

  • WordPress page caching: when dynamic content breaks static delivery

    WordPress page caching: when dynamic content breaks static delivery

    WordPress page caching: when dynamic content breaks static delivery
    Photo: Rami via Wikimedia Commons (Public domain)

    Page caching is one of the fastest ways to speed up a WordPress site. Instead of rebuilding every page on every visit, the server delivers a static HTML snapshot. For content-driven sites, it’s a performance multiplier—until it breaks something you didn’t realize was dynamic.

    The promise is simple: cache the page once, serve it to everyone. The problem is that “everyone” includes logged-in users, people in different time zones, and visitors who should see personalized content. When you turn on full-page caching without understanding what gets frozen, you end up serving stale data to the wrong people.

    What page caching actually does

    Most caching plugins—WP Rocket, LiteSpeed Cache, W3 Total Cache—generate a static HTML file the first time someone visits a page. Every subsequent visitor gets that file until it expires or you manually clear the cache. The PHP engine never runs. The database never gets queried. It’s fast because it skips WordPress entirely.

    That works fine for a blog post that doesn’t change. It breaks the moment you add anything that should look different for different visitors: a “Welcome back, [Name]” message, a shopping cart count, a members-only section, a countdown timer, or even a “currently online” widget.

    The static HTML file doesn’t know who’s visiting. It shows the same cached snapshot to everyone—logged in or not, subscriber or guest, New York or Tokyo.

    What breaks first

    The most common casualties are widgets and sidebar elements that rely on server-side state. A “recent comments” block might show the same five comments for hours, even as new ones come in. A “popular posts” widget freezes at whatever was popular when the cache was built. If you’re running ads or affiliate content that rotates server-side, the same banner gets served to everyone until the cache clears.

    Logged-in users see the logged-out version of the page unless you explicitly exclude them from caching. That means no admin bar, no edit links, no personalized menus. If you’re running a membership site or a course platform, this is a deal-breaker. Members land on a page that tells them to log in, even though they already are.

    Checkout pages, account dashboards, and cart views also break under full-page caching. Most plugins auto-exclude /cart/ and /checkout/ paths, but if you’re using a custom URL structure or a non-standard plugin, you need to add exclusions manually.

    How to cache without breaking dynamic content

    The cleanest fix is to separate static and dynamic elements. Cache the page structure—header, footer, main content—and load personalized pieces via JavaScript after the page renders. Most modern membership plugins and ecommerce platforms do this by default: the page loads instantly from cache, then an AJAX call fetches the cart count or user name and injects it into the DOM.

    If you’re building custom features, use the same pattern. Cache the full page, but add a data-user-id attribute to any personalized container and populate it client-side with a lightweight API call. Keep that endpoint uncached—exclude /wp-json/ routes or use a dedicated AJAX handler.

    For logged-in users, most caching plugins offer a “don’t cache for logged-in users” toggle. Enable it if your site has frequent admin activity or if subscribers expect personalized views. The trade-off is that logged-in traffic bypasses the cache entirely, so page speed drops for those visitors. If you have a small team and most traffic is anonymous, this is fine. If half your visitors are logged in, you’re losing most of the performance benefit.

    Time-sensitive content—countdowns, limited offers, live event notices—should either be excluded from caching or rendered client-side. A cached page with a countdown timer will show the same time to everyone until the cache expires, which defeats the urgency. Use JavaScript to calculate the time difference in the browser, or exclude the page from caching and accept the performance hit.

    Testing what actually gets cached

    The easiest way to catch caching issues before they go live: visit your site in an incognito window, clear the cache, reload the page, then log in and reload again. If the logged-in view looks identical to the logged-out view, something’s wrong. Check the page source—if you see on a logged-in page, you need to adjust your exclusion rules.

    Most caching plugins log which pages get cached and which get bypassed. WP Rocket and LiteSpeed Cache both show cache hit/miss stats in their dashboards. If a checkout page shows a cache hit, you know it’s misconfigured.

    For membership sites, test with a subscriber account. Log in, navigate to a members-only page, then open the same URL in a private window. If the private window shows the member content, the page was cached while you were logged in and is now being served to everyone. Add that path to your exclusion list immediately.

    Page caching is worth the setup cost—most WordPress sites see 40–60% faster load times once it’s configured correctly. But if you ship personalized content, ecommerce, or logged-in experiences, the default settings will break something. Test every dynamic element, exclude what needs to stay fresh, and move the rest to client-side rendering.

    Got a caching setup that works for your WordPress site? Reply and let us know which plugin you’re using and how you handle logged-in users—we’ll feature the best setups in a future roundup.

  • WordPress REST API authentication: tokens vs. cookies vs. nonces

    WordPress REST API authentication: tokens vs. cookies vs. nonces

    WordPress REST API authentication: tokens vs. cookies vs. nonces
    Photo: Bilal Elmoussaoui and Authenticator contributors via Wikimedia Commons (GPLv3)

    If you’re building custom workflows on top of WordPress—feeding published posts into a social scheduler, syncing custom fields to an external CRM, or triggering email sends when a post goes live—you’re probably using the REST API. And authentication is where most operators hit a wall.

    WordPress offers three main authentication methods for REST API requests: application passwords (tokens), cookie authentication, and nonces. Each behaves differently, and choosing the wrong one means your automation fails silently or exposes your site to unnecessary risk.

    Application passwords: the safe default for external tools

    Application passwords were added to WordPress core in version 5.6. They’re revocable tokens tied to a specific user account, designed for external applications that need programmatic access without exposing your actual login password.

    When you generate an application password from your user profile, WordPress creates a 24-character token. You pass it via HTTP Basic Auth in every API request. If your workflow tool gets compromised or you stop using it, you revoke the token—your main password stays intact.

    This is the right choice for any tool that lives outside WordPress: Zapier workflows, custom Node.js scripts, Python automation, or third-party SaaS that needs to read or write data. The token can’t be used to log into the WordPress admin, only to authenticate API calls.

    Non-obvious gotcha: Application passwords only work over HTTPS. If your staging site uses HTTP, authentication will fail with a vague 401 error. WordPress blocks the feature entirely on non-encrypted connections.

    Cookie authentication: built-in, but brittle for automation

    Cookie authentication is what WordPress uses when you’re logged into the admin and browse the site. Your session cookie proves who you are. The REST API respects that cookie, so any JavaScript running on your own site—inside the WordPress admin or on the front end—can make authenticated requests without extra setup.

    This works fine for plugins or custom admin dashboards. But it’s unreliable for external automation. Cookies expire. They don’t travel well across domains. And if you’re running a headless setup or calling the API from a server, cookies don’t exist.

    Cookie-based requests also require a valid nonce for any write operation (POST, PUT, DELETE). The nonce is a time-limited token WordPress generates to prevent cross-site request forgery. It’s automatically included in admin-area JavaScript via wp_localize_script(), but if you’re building a custom front-end interface, you need to fetch and attach it manually.

    When to use it: Custom admin-area tools, React-based dashboards embedded in WordPress, or AJAX requests from logged-in users on the front end. Not for cron jobs, external services, or anything that runs without an active browser session.

    Nonces: not authentication, just anti-forgery

    Nonces are often confused with authentication, but they’re not. A nonce proves a request came from your site—not from a malicious third-party form. It doesn’t prove who sent the request; it just checks that the request originated from a legitimate WordPress-generated page.

    Nonces expire after 24 hours by default (technically 12–24 hours depending on when they were generated). If your automation fetches a nonce and then waits two days to use it, the request fails.

    You generate a nonce in PHP with wp_create_nonce('action-name') and validate it with wp_verify_nonce(). The REST API checks nonces automatically when you use cookie authentication, but only for destructive operations. Read-only GET requests don’t need one.

    Common mistake: Hardcoding a nonce into a JavaScript file. It expires, and your AJAX calls start failing silently. Always generate nonces dynamically and pass them to your script at page load.

    Which one to use

    If you’re calling the WordPress REST API from outside WordPress—Zapier, a headless front end, a Python script, a mobile app—use application passwords. Generate one per tool, label it clearly, and revoke it when you’re done.

    If you’re building a feature inside WordPress—a custom admin page, a front-end dashboard for logged-in users—use cookie authentication. WordPress handles the session for you. Just remember to include a nonce for write operations.

    If you’re passing data between WordPress and an external service that you control, consider setting up a custom endpoint with a shared secret instead of relying on user-based authentication. Store the secret in an environment variable, check it in your endpoint logic, and skip the user-permission overhead entirely.

    Most authentication failures in WordPress automations come from mixing these methods or assuming cookies work outside the browser. Pick the method that matches where your code runs, and half your API errors disappear.

    Got a WordPress automation question? Reply to this email—we cover one reader question every Sunday.

  • WordPress multisite subdomain DNS: how wildcard records actually work

    WordPress multisite in subdomain mode lets you spin up site1.example.com, site2.example.com, and so on—all from a single WordPress install. It’s powerful for niche site portfolios, client networks, or SaaS-style content platforms. But the DNS setup trips up even experienced operators, especially when subdomains don’t resolve or SSL certificates fail to provision.

    Here’s how the wildcard DNS record actually works, what propagation looks like in practice, and the edge cases that break automated SSL issuance.

    What the wildcard A record does

    When you configure WordPress multisite in subdomain mode, you add a single DNS record at your registrar or DNS provider:

    *.example.com A 203.0.113.45

    That asterisk is a wildcard. It tells DNS resolvers: “any subdomain that doesn’t have its own explicit record should point to this IP address.” So blog.example.com, shop.example.com, and anythingyouwant.example.com all resolve to the same server—your WordPress host.

    The WordPress application then inspects the Host header in each HTTP request and serves the correct site from its internal database. The DNS layer doesn’t know or care which subdomains exist; it just routes everything to the same place.

    Propagation timing and the root domain exception

    Wildcard DNS propagates like any other record—typically within minutes to a few hours, depending on TTL and resolver caching. But two gotchas appear frequently:

    The root domain doesn’t match the wildcard. If you have an existing A record for example.com pointing to a different IP (say, a marketing site on a separate host), that takes precedence. The wildcard only catches subdomains. If you want example.com itself to serve a multisite network site, you need a separate A record for the root, and it must point to the same IP as the wildcard.

    Explicit subdomain records override the wildcard. If you previously set up mail.example.com A 198.51.100.10 for an email service, that record wins. The wildcard only applies when no more-specific record exists. Audit your DNS zone file before enabling multisite—old staging subdomains or forgotten services can create confusing “site not found” errors.

    SSL certificate provisioning and wildcard complications

    Most managed WordPress hosts and CDNs (Cloudflare, Kinsta, WP Engine) offer automatic Let’s Encrypt SSL. But wildcard certificates require DNS-01 challenge validation, not the simpler HTTP-01 method.

    Here’s what that means in practice:

    • Single-site certificates use HTTP-01: Let’s Encrypt places a file at example.com/.well-known/acme-challenge/token, retrieves it, and issues the cert. Takes seconds.
    • Wildcard certificates use DNS-01: Let’s Encrypt asks you to create a TXT record at _acme-challenge.example.com, waits for propagation, validates it, then issues. This requires API access to your DNS provider, which not all hosts support automatically.

    If your host doesn’t support wildcard SSL automation, you have two options:

    1. Manually provision wildcard certs every 90 days (painful).
    2. Use a reverse proxy like Cloudflare in front of WordPress, letting Cloudflare handle wildcard SSL termination. Traffic flows: visitor → Cloudflare (SSL) → origin server (can be HTTP or a Cloudflare-issued origin cert).

    Cloudflare’s free tier includes wildcard SSL and works well for multisite operators who don’t need enterprise SLA guarantees. Just ensure SSL/TLS mode is set to “Full” or “Full (strict)”—”Flexible” mode (Cloudflare-to-visitor encrypted, Cloudflare-to-origin unencrypted) creates mixed-content warnings and breaks WordPress admin over HTTPS.

    When new subsites don’t resolve immediately

    You create a new subsite in WordPress, visit newsite.example.com, and get a DNS error. The wildcard’s already in place—what’s wrong?

    Two common causes:

    Local DNS cache. Your machine or router cached a previous NXDOMAIN (non-existent domain) response. Flush your local DNS cache (sudo dscacheutil -flushcache on macOS, ipconfig /flushdns on Windows) or wait 5–15 minutes.

    CAA records blocking SSL issuance. If you have a CAA record at the root domain restricting which certificate authorities can issue certs (e.g., example.com CAA 0 issue "letsencrypt.org"), and your host uses a different CA or expects wildcard issuance, the cert request fails silently. Check your DNS zone for CAA records if SSL won’t provision for new subsites.

    One non-obvious tip: use a staging wildcard on a separate domain

    If you’re testing multisite before going live, don’t use a subdomain of your production domain—use a completely separate domain or a .test suffix with local /etc/hosts entries. Why? Because once you add the wildcard A record to your live domain, every possible subdomain resolves, including ones you haven’t created yet. That can expose staging sites to search engines or curious visitors poking around common subdomain names like staging.example.com or dev.example.com.

    A safer pattern: register example-staging.com, apply the wildcard there, and test your network in isolation. When ready, migrate to the production domain with confidence that DNS and SSL won’t surprise you.

    Got a WordPress multisite setup question? Hit reply—we’d love to feature your scenario in a future Q&A piece.

  • WordPress maintenance mode locks out admins—here’s why

    WordPress maintenance mode locks out admins—here’s why

    WordPress maintenance mode locks out admins—here's why
    Photo by Mike Szczepanski on Unsplash

    You activate a WordPress plugin update, see the “briefly unavailable for scheduled maintenance” message, and refresh a minute later expecting your dashboard. Instead, you’re staring at a blank maintenance page—with no login link, no bypass, and no way back in.

    This isn’t a hypothetical edge case. Maintenance mode lockouts happen when a plugin update stalls, a server timeout interrupts file writes, or a third-party maintenance plugin misconfigures its access rules. The site enters maintenance mode, but the cleanup script that’s supposed to disable it never runs.

    Here’s what actually happens, when you’re at risk, and how to fix it without SSH or panic.

    What triggers WordPress maintenance mode

    WordPress core uses a file called .maintenance in your site’s root directory to signal that updates are in progress. When you trigger a plugin or theme update from the dashboard, WordPress writes this file, runs the update, then deletes the file. The entire process usually takes 10–30 seconds.

    If something interrupts that flow—a PHP timeout, a failed database query, a hosting firewall rule that kills long-running requests—the .maintenance file stays in place. WordPress reads it on every page load and shows the maintenance message instead of your site.

    Crucially, this affects admin URLs too. The maintenance check happens before WordPress loads wp-admin, so even /wp-login.php returns the same generic holding page. No login form. No dashboard. No escape hatch.

    When third-party maintenance plugins make it worse

    Plugins like WP Maintenance Mode and Coming Soon Page add their own maintenance layers. They write database options or create custom .maintenance files with extended logic. Some let you whitelist IP addresses or set a bypass secret; others don’t.

    The problem: if you activate one of these plugins and misconfigure the access rules—or if the plugin conflicts with a caching layer—you can lock yourself out without triggering a core WordPress update at all. The plugin’s maintenance screen overrides everything, including admin access, and you’re left guessing which file or database row is responsible.

    How to recover access in under five minutes

    If you’re locked out, you need file-level access. Most shared hosting control panels (cPanel, Plesk, Flywheel, BigScoots) include a file manager. Open it, navigate to your WordPress root directory (usually public_html or www), and look for .maintenance. Delete it. Refresh your site. You’re back in.

    If the lockout persists, a plugin is holding the maintenance state. Connect via SFTP or file manager and rename the plugin’s folder inside /wp-content/plugins/. WordPress will deactivate it automatically. If you don’t know which plugin caused it, rename the entire plugins folder to plugins-off, log back in, then rename it back and reactivate plugins one at a time.

    For sites using object caching (Redis, Memcached), flush the cache from your hosting dashboard or via WP-CLI. Cached maintenance flags sometimes outlive the .maintenance file itself.

    One non-obvious tip: set a cron failsafe

    If you run a solo site or small team operation, add a cron job that deletes .maintenance files older than five minutes. Most hosting control panels let you schedule shell commands. A one-liner like find /path/to/wordpress -name .maintenance -mmin +5 -delete prevents lockouts from becoming multi-hour emergencies.

    For managed WordPress hosts that don’t expose cron directly, some monitoring services (like UptimeRobot or Oh Dear) can trigger webhook alerts when your site returns a maintenance header for longer than expected. You won’t auto-recover, but you’ll know within minutes instead of discovering it when a reader emails you.

    Maintenance mode is a necessary safety mechanism, but it’s designed for automated workflows—not manual bailouts. The faster you recognize a lockout, the less downtime you’ll eat.

    Want more WordPress infrastructure breakdowns? Reply with the hosting gotcha that cost you the most downtime—we’ll cover it in a future issue.

  • WordPress object cache: when persistent caching breaks your site

    WordPress object cache: when persistent caching breaks your site

    WordPress object cache: when persistent caching breaks your site
    Photo by Luke Chesser on Unsplash

    Most WordPress performance guides tell you to enable persistent object caching with Redis or Memcached. The promise: fewer database queries, faster page loads, happier visitors. The reality: if you don’t understand what WordPress caches in memory—and what shouldn’t be cached—you’ll spend hours debugging stale data, broken admin screens, and plugins that stop working.

    Persistent object caching is a feature worth using, but only if your site actually needs it and your stack can handle the edge cases. Here’s how it works, when to turn it on, and the one non-obvious setting that prevents most breakage.

    What WordPress object caching actually does

    By default, WordPress caches objects—database query results, option lookups, taxonomy terms—in PHP memory for the duration of a single page load. Once the request ends, the cache clears. Every new visitor triggers fresh queries.

    Persistent object caching extends that lifetime. Instead of storing cached data in PHP’s runtime memory, WordPress writes it to Redis or Memcached, which live outside the PHP process. The cache survives across requests, across visitors, across server restarts (until you flush it or it expires).

    For high-traffic sites, this cuts database load dramatically. A single post might generate 50+ queries on an uncached load. With object caching, most of those queries hit Redis instead of MySQL. Page generation drops from 800ms to 200ms.

    For low-traffic sites—under 10,000 monthly visits—the difference is negligible. Your database isn’t the bottleneck; network latency and unoptimized images are. Adding Redis adds complexity without measurable speed gains.

    When persistent caching breaks things

    The most common failure mode: stale data that won’t refresh. You update a post, but the homepage still shows the old title. You change a user role, but the admin menu doesn’t reflect the new permissions. You deactivate a plugin, but its settings page still appears.

    This happens because WordPress—and many plugins—assume the object cache clears between requests. They write data once, cache it aggressively, and never invalidate it. When you enable persistent caching, that assumption breaks.

    WooCommerce is notorious for this. Product stock counts, cart totals, and session data get cached with long expiration times. If your cache invalidation logic doesn’t account for stock changes, customers see inaccurate inventory. The fix: WooCommerce-specific cache groups marked as non-persistent, or a plugin like Object Cache Pro that handles WooCommerce edge cases automatically.

    Membership and LMS plugins—LearnDash, MemberPress, Restrict Content Pro—cache user permissions and course progress. If the cache doesn’t flush when a user upgrades or completes a lesson, access control breaks. You’ll get support tickets claiming “I paid but still can’t access the content.”

    The second failure mode: cache key collisions in multisite or multi-environment setups. If your staging and production sites share the same Redis instance without unique key prefixes, updating content on staging can overwrite production cache entries. Your live site serves draft content or test data.

    The non-obvious setting: cache group exclusions

    Most object cache drop-ins—Redis Object Cache, Memcached Object Cache, Object Cache Pro—let you exclude specific cache groups from persistence. These groups fall back to runtime-only caching, just like default WordPress behavior.

    The groups you almost always want to exclude:

    • counts — post counts, comment counts, term counts. These change frequently and queries are cheap.
    • plugins — plugin metadata. Caching this causes deactivated plugins to linger in memory.
    • themes — same reason. Theme switches won’t take effect until you manually flush.
    • userlogins and userslugs — user session and authentication data. Caching these across requests creates security risks.

    If you’re running WooCommerce, also exclude:

    • wc_session_id
    • wc_cart_hash
    • wc_reserved_stock

    For membership plugins, exclude any group containing “user_meta” or “permissions” in the name. Check your plugin’s documentation—most maintain a list of recommended exclusions.

    In the Redis Object Cache plugin (the most popular free option), you set exclusions via the WP_REDIS_IGNORED_GROUPS constant in wp-config.php:

    define('WP_REDIS_IGNORED_GROUPS', ['counts', 'plugins', 'themes', 'userlogins', 'userslugs']);

    Object Cache Pro uses a similar constant: OCP_IGNORED_GROUPS.

    When to actually enable persistent caching

    Turn it on if:

    • Your site gets more than 50,000 pageviews per month, or you run resource-intensive queries (complex WP_Query loops, custom taxonomies with thousands of terms).
    • Your hosting plan includes Redis or Memcached as a managed service. Most managed WordPress hosts—Kinsta, WP Engine, Flywheel—provision Redis automatically. If you’re on shared hosting or a basic VPS, setting up Redis yourself adds maintenance overhead.
    • You can test on staging first and you have a cache flush strategy. Automatic purging on post updates is table stakes; you also need manual flush access for emergencies.

    Skip it if:

    • Your site is low-traffic or you’re already using a full-page cache (WP Rocket, LiteSpeed Cache, Cloudflare). Full-page caching delivers bigger performance wins with fewer edge cases.
    • You run a membership, e-commerce, or LMS site and you don’t have time to debug cache invalidation issues. The risk of serving stale user-specific data outweighs the speed benefit.
    • Your host doesn’t offer Redis, and you’re not comfortable managing it yourself. A misconfigured Redis instance—no password, exposed port, no memory limits—is a security and stability liability.

    Want more hosting and performance breakdowns like this? Subscribe to One Two Three Send—we dig into the infrastructure decisions solo operators actually face, no enterprise fluff.

    Persistent object caching works when your site’s traffic and query complexity justify it, and when you’ve accounted for the plugins that assume caches don’t persist. If you’re enabling it just because a checklist told you to, you’re adding risk without reward.

  • WordPress image optimization plugins: WebP vs. AVIF in 2026

    WordPress image optimization plugins: WebP vs. AVIF in 2026

    WordPress image optimization plugins: WebP vs. AVIF in 2026
    Photo by Growtika on Unsplash

    Image optimization plugins for WordPress promise faster page loads and better Core Web Vitals scores. Most now offer WebP conversion by default, and a growing number add AVIF support. But the two formats solve different problems, and choosing the wrong one can break images for a slice of your traffic or bloat your hosting bill.

    Here’s how to pick the right format—and when to use both.

    WebP: mature, compatible, predictable file sizes

    WebP has been around since 2010. Browser support hit critical mass years ago: Chrome, Firefox, Safari, and Edge all support it natively. According to Can I Use data, WebP coverage sits above 97% of global users in 2026.

    WebP handles both lossy and lossless compression. For most photography and blog post images, WebP lossy at 80–85 quality produces files 25–35% smaller than JPEG at equivalent visual quality. Lossless WebP beats PNG for screenshots and graphics with transparency, though the size advantage is smaller—usually 15–20%.

    The format supports alpha-channel transparency, which makes it a direct PNG replacement. If your site uses transparent logos, icons, or overlays, WebP won’t break them.

    Most WordPress image optimization plugins—ShortPixel, Imagify, Smush, EWWW Image Optimizer—convert to WebP automatically. The format is stable, well-tested, and won’t surprise you with edge cases.

    AVIF: smaller files, slower encoding, spottier support

    AVIF arrived later—Chrome added support in 2020, Safari in 2021. Browser coverage in 2026 hovers around 92%, which means 8% of visitors still can’t decode it. That gap includes older Android devices and some corporate-locked browsers.

    AVIF’s advantage is file size. At equivalent quality settings, AVIF images run 20–30% smaller than WebP and 40–50% smaller than JPEG. For image-heavy sites—portfolios, e-commerce galleries, photo blogs—that difference compounds fast.

    The tradeoff is encoding time. Generating AVIF files takes 3–5× longer than WebP on most hosting environments. If your plugin converts images on-upload or via cron job, AVIF processing can time out on shared hosting or budget VPS plans. Plugins like ShortPixel and Imagify offload conversion to their own servers, which sidesteps the timeout problem but adds API dependency.

    AVIF also handles transparency, but browser support for alpha-channel AVIF lags slightly behind opaque AVIF. If you’re converting transparent PNGs to AVIF, test rendering in Safari and Firefox before deploying site-wide.

    When to use WebP, AVIF, or both

    If your hosting plan is shared or low-tier VPS, stick with WebP. AVIF encoding can spike CPU usage and trigger resource limits. WebP delivers most of the file-size benefit without the processing cost.

    If you’re on managed WordPress hosting or a dedicated server with headroom, enable AVIF with WebP fallback. Most modern plugins support this: serve AVIF to browsers that accept it, WebP to the rest, and original JPEG/PNG as a last resort. The plugin detects support via the Accept HTTP header or generates a <picture> element with multiple <source> tags.

    For e-commerce sites with hundreds of product images, AVIF’s size advantage matters. A 30% reduction in image payload can shave a full second off Largest Contentful Paint on mobile. That translates to measurable conversion lift. Just make sure your plugin queues conversions in the background—batch-converting 500 images at once will lock up most hosting environments.

    For blogs with a few images per post, WebP is enough. The complexity of AVIF fallback chains and the 8% browser gap aren’t worth the incremental file-size win.

    One plugin detail that trips people up

    Most WordPress image optimization plugins store multiple formats of the same image—original JPEG, WebP, and AVIF—on disk. A single uploaded image can generate three or four files, depending on your settings. If you’re on a hosting plan with tight disk quotas, this adds up fast. A 2 GB media library can balloon to 5–6 GB after enabling both WebP and AVIF.

    Check your plugin’s settings for an option to delete originals after conversion. Some plugins keep the source file for rollback purposes; others let you purge it once the optimized versions are live. If disk space is tight, delete originals—but keep a local backup of your media library first.

    Want more tool breakdowns like this? Subscribe to One Two Three Send for weekly deep-dives on the platforms and plugins that run online businesses.

  • WordPress REST API rate limits: what trips automated workflows

    WordPress REST API rate limits: what trips automated workflows

    WordPress REST API rate limits: what trips automated workflows
    Photo: Matinbeigi via Wikimedia Commons (CC BY-SA 4.0)

    If you’re running any kind of automated workflow that touches WordPress—content sync tools, headless front-ends, analytics dashboards, social schedulers—you’re making REST API requests. And sooner or later, you’ll hit a wall you didn’t know existed.

    Most managed WordPress hosts impose rate limits on REST API calls. The problem: they rarely document the exact threshold, and when you exceed it, the failure mode is rarely obvious. Your workflow just… stops working.

    Where the ceiling sits

    Rate limits vary wildly by host and plan tier. WP Engine enforces approximately 60 requests per minute per IP on mid-tier plans. Kinsta’s limit sits closer to 100 requests per minute but drops during peak traffic windows. SiteGround and Bluehost shared plans can throttle as low as 30 requests per minute under load.

    The limit isn’t always per-account—it’s often per-IP or per-site. If you’re running multiple automations from the same server (a Zapier workflow pulling post data while a headless front-end fetches metadata), both count toward the same ceiling.

    BigScoots and similar VPS-adjacent hosts give you more headroom, but you’re still sharing infrastructure unless you’ve negotiated custom limits or moved to dedicated resources.

    How you trip it without noticing

    The most common culprit: bulk operations during content imports or migrations. If you’re syncing 500 posts from an external CMS via REST API, and each post requires three API calls (create post, upload featured image, assign taxonomy terms), that’s 1,500 requests. At 60 requests per minute, you’re looking at 25 minutes—but only if nothing else touches the API during that window.

    Social scheduling tools that auto-pull post excerpts or featured images can silently hammer the API every time they refresh your queue. Analytics plugins that log every page view via REST API can push you over the edge during traffic spikes.

    Headless WordPress setups are especially vulnerable. If your Next.js or Gatsby build process fetches all posts, taxonomies, and media in parallel during deployment, you can blow through your limit in seconds. Incremental static regeneration helps, but only if you’ve tuned the request concurrency.

    How to test your actual limit

    Most hosts won’t tell you the number until you ask support directly—and even then, the answer is often vague (“we recommend staying under 60 requests per minute”). The fastest way to find your real ceiling: controlled load testing.

    Use a tool like curl with a loop, or a lightweight Node script with axios, to hit a non-destructive endpoint (/wp-json/wp/v2/posts?per_page=1) at increasing rates. Start at 30 requests per minute, then 60, then 100. Watch for HTTP 429 responses or sudden timeouts.

    Log the exact rate where you start seeing failures. That’s your ceiling. Build in a 20% buffer—if you hit 429 at 80 requests per minute, design your workflows to stay under 65.

    The non-obvious fix

    Most developers reach for caching or request batching first. Both help, but the real leverage is in request sequencing and backoff logic.

    If you’re building a custom integration, implement exponential backoff: when you hit a 429, wait two seconds, then four, then eight. Most WordPress hosts reset rate-limit counters every 60 seconds, so a brief pause often resolves the issue without killing your workflow.

    For third-party tools (Zapier, Make, n8n), check if they expose retry settings or rate-limit handling. Zapier’s “Delay After Queue” action lets you throttle outbound requests manually. Make’s HTTP module supports custom retry logic via error handlers.

    If you’re running a headless build process, switch from parallel to sequential fetching for high-volume endpoints, or split your build into smaller incremental chunks. Gatsby’s GATSBY_CONCURRENT_DOWNLOAD environment variable lets you cap parallel requests; start at 10 and tune down if you’re still hitting limits.

    One last thing: if you’re on a managed host and legitimately need higher limits for a production workflow, ask. Most hosts will raise the ceiling for established accounts with predictable traffic patterns—but only if you ask before you break something.

    Have a question about WordPress infrastructure or automation limits? Reply to this email—we cover reader questions every Sunday.