Category: Hosting

  • WordPress database backups: snapshot frequency vs. disk cost

    WordPress database backups: snapshot frequency vs. disk cost

    WordPress database backups: snapshot frequency vs. disk cost
    Photo by Woliul Hasan on Unsplash

    Most WordPress operators set a backup schedule once and forget it. Daily snapshots sound safe. Hourly feels safer. But backup frequency compounds fast—and if you’re running on managed WordPress hosting or a VPS, you’re paying for every copy stored.

    The question isn’t whether to back up. It’s how often you need to, what retention window actually protects you, and when snapshot frequency starts costing more than the risk it mitigates.

    Backup frequency changes your storage footprint

    A typical WordPress database for a content site with 500 posts, 2,000 comments, and standard plugin metadata runs between 15 MB and 50 MB. If you’re running WooCommerce, memberships, or forum software, double or triple that.

    Hourly backups with 30-day retention mean 720 snapshots per month. At 30 MB per snapshot, that’s 21 GB of backup storage. Most managed hosts allocate 10–20 GB of backup space before charging overage fees—usually $0.10 to $0.25 per GB per month.

    Switch to daily backups with the same retention, and you drop to 30 snapshots: 900 MB total. The difference is $2 to $5 per month for a small site, but $20+ if your database crosses 200 MB.

    Backup plugins like UpdraftPlus and BackWPup let you set different intervals for database vs. files. Your database changes every time someone comments, subscribes, or places an order. Your theme files don’t. Splitting the schedule—hourly database, weekly files—cuts storage costs without losing transaction-level recovery.

    Retention windows protect different failure modes

    Thirty-day retention is a convention, not a requirement. What you’re protecting against determines how far back you need to go.

    If you’re worried about a bad plugin update or a botched migration, you need snapshots from the last 48 hours. Anything older than a week is archaeological. A 7-day retention window with hourly backups gives you 168 recovery points and costs one-quarter the storage of a 30-day window.

    If you’re worried about silent data corruption—broken automation, spam injection, or a membership plugin quietly deleting records—you need longer retention, but you don’t need high frequency. Weekly backups kept for 90 days let you spot patterns and roll back months without paying for hundreds of hourly snapshots.

    The hybrid approach: hourly backups kept for 7 days, plus one weekly backup kept for 12 weeks. That’s roughly 180 snapshots instead of 720, and it covers both rapid rollback and long-term forensics.

    Incremental vs. full snapshots: what your plugin actually stores

    Not all backup plugins store full copies every time. Incremental backups save only the database rows that changed since the last snapshot. UpdraftPlus, BlogVault, and most enterprise WordPress backup tools support incremental mode.

    For a content site publishing twice a week, an incremental backup after the first full snapshot might be 500 KB instead of 30 MB. Over a month, that shrinks your storage footprint by 80% or more.

    The tradeoff: incremental backups depend on the chain. If snapshot #4 is corrupted, you can’t restore from snapshot #10 without it. Some plugins automatically store a weekly full backup alongside incrementals to break the dependency chain. Check your plugin’s settings—most don’t enable this by default.

    When to pay for more frequent backups

    If you’re processing transactions, taking payments, or running time-sensitive campaigns, hourly backups are worth the cost. Losing six hours of WooCommerce orders or sponsor sign-ups isn’t a storage-cost discussion—it’s a revenue-loss discussion.

    If you’re publishing a content site with no e-commerce and infrequent comments, daily backups are enough. Your exposure window is the time between backups, and for most solo operators, a day of republishing is tolerable compared to the compounding cost of storing 720 snapshots you’ll never use.

    One non-obvious tell: check your WordPress database activity log (if your host provides one, or install Query Monitor for a week). If 95% of your write queries happen during your publishing window—say, Tuesday and Thursday mornings—you don’t need constant snapshots. Schedule backups for right after you publish, and drop frequency the rest of the week.

    What breaks when you skimp

    The biggest failure mode isn’t missing a backup window. It’s discovering your backups don’t restore.

    Twice a year, test a restore to a staging environment. Download a snapshot, spin up a local WordPress instance or a staging server, and import it. If your backup plugin stores files and database separately, make sure both pieces actually reconnect. I’ve seen operators keep 90 days of database snapshots that restored to a blank site because the file backup was misconfigured.

    Second most common break: your backup plugin times out mid-snapshot because your host limits PHP execution to 60 seconds and your database takes 90 to export. This fails silently unless you check logs. Solution: switch to a plugin that chunks exports (UpdraftPlus and BackWPup both do this), or move to host-level backups if your provider offers them.

    Want more infrastructure breakdowns like this? Subscribe to One Two Three Send—we cover the WordPress tooling, hosting gotchas, and cost optimizations that solo operators actually run into.

  • WordPress CDN purge delays: why cache clears take five minutes

    WordPress CDN purge delays: why cache clears take five minutes

    WordPress CDN purge delays: why cache clears take five minutes
    Photo: Matinbeigi via Wikimedia Commons (CC BY-SA 4.0)

    You hit “Publish” on a post correction, purge the CDN cache, refresh the page—and the typo’s still there. You wait. Refresh again. Still wrong. Five minutes later, it updates.

    CDN cache purging isn’t instant. Most providers propagate purge commands through distributed edge networks, and that takes time. If you’re running a content business on WordPress, understanding actual purge timing prevents panic refreshes and helps you decide when a full-site purge is worth the performance cost.

    How CDN purge propagation actually works

    When you purge a cached object—a page, an image, a CSS file—the CDN doesn’t instantly delete it from every edge server worldwide. Instead, your purge request hits the CDN’s API, gets queued, then propagates to regional points of presence (PoPs). Each PoP marks the object stale or removes it, depending on the provider’s architecture.

    Cloudflare’s purge-by-URL typically completes in 30 seconds globally, though they quote “up to 30 seconds” as a best-case median. Fastly averages under five seconds for most objects but can stretch to 15–20 seconds during high load. BunnyCDN usually clears within 10–30 seconds. StackPath and KeyCDN sit around 60–90 seconds for full propagation.

    Those are averages. Edge cases—literal and figurative—happen. A PoP in Sydney might lag behind one in Frankfurt. If your reader’s browser cached the HTML before the purge, they’ll still see stale content until their local cache expires or they hard-refresh.

    What happens during the lag

    Between purge initiation and full propagation, some visitors see the old version, others see the new one. If you’re fixing a pricing error or a broken affiliate link, that inconsistency matters.

    WordPress caching plugins like WP Rocket, W3 Total Cache, and LiteSpeed Cache trigger CDN purges via API when you update a post. But they don’t wait for confirmation. The plugin fires the request, WordPress shows you the updated post in the admin, and you assume it’s live everywhere. It’s not—yet.

    If you need guaranteed consistency, purge the specific URL, wait two minutes, then check from an incognito window or a mobile device on cellular (different network path, different edge server). Don’t rely on your own cached browser session.

    Selective purge vs. full-site purge

    Most WordPress caching plugins offer two purge modes: purge this page, or purge everything. Purging a single URL is fast and surgical. Purging the entire cache forces every page, image, and asset to reload from your origin server until the CDN re-caches them.

    A full-site purge sounds safe—no stale content anywhere—but it costs you. For the next 10–30 minutes, your origin server handles traffic it normally wouldn’t. If you’re on shared hosting or a low-tier VPS, that spike can slow response times or trigger rate limits. BigScoots and other managed hosts handle it better, but you still lose the speed benefit of edge caching until the CDN rewarms.

    Purge selectively unless you’ve changed something global: a CSS file, a header template, or site-wide navigation. If you fixed one post’s typo, purge that post’s URL and its homepage listing. That’s it.

    When browser cache makes CDN purge irrelevant

    CDN purge only matters if the CDN is the bottleneck. If you set aggressive browser cache headers—say, one year for HTML—visitors won’t even request the page from the CDN. Their browser serves the cached copy until the cache-control max-age expires.

    Check your caching plugin’s browser cache settings. WP Rocket defaults to one year for static assets (images, CSS, JS) but typically one hour or less for HTML. LiteSpeed Cache lets you set per-file-type TTLs. If your HTML cache TTL is longer than your CDN purge time, the CDN purge won’t help users who visited before the update.

    The fix: keep HTML browser cache short (3600 seconds or less) or version your URLs when you make critical changes. Most plugins handle CSS/JS versioning automatically via query strings, but HTML doesn’t get that treatment.

    Tool-specific purge behavior

    Cloudflare’s WordPress plugin purges the updated post, the homepage, any archive pages that list it, and related tag/category pages. Selective, but broader than just the post URL. Fastly’s plugin (less common) purges by surrogate key, which batches related objects—faster propagation but requires tagging setup.

    WP Rocket integrates with Cloudflare, StackPath, and Sucuri CDNs via API. When you save a post, it purges the post URL and any pages you’ve configured in settings. W3 Total Cache supports a longer CDN list but requires manual API key entry and sometimes fails silently if credentials expire.

    If purges aren’t working, check your plugin’s CDN settings. API tokens expire, rate limits get hit, and some hosts block outbound API calls from PHP. You’ll see no error in WordPress—just stale content that won’t budge.

    Want more infrastructure breakdowns like this? Subscribe to One Two Three Send for weekly deep-dives into the tools that run your online business—no fluff, just what actually happens under the hood.

  • WordPress staging environment sync: when to push down vs. pull up

    WordPress staging environment sync: when to push down vs. pull up

    WordPress staging environment sync: when to push down vs. pull up

    Most WordPress hosts now include staging environments—a cloned copy of your live site where you can test plugin updates, theme changes, or new integrations without risking downtime. The setup is simple: click a button, wait a few minutes, and you’ve got a sandbox.

    But the real decision isn’t whether to use staging. It’s which direction to sync, and when.

    Most operators treat staging as a one-way mirror: clone production down, test changes, push them back up. That works for plugin updates and design tweaks. But it breaks down when your live site has dynamic data—new posts, form submissions, user signups, or orders—that didn’t exist when you cloned.

    Here’s how to decide which sync direction fits your workflow, and what breaks when you pick wrong.

    Pushing down: when fresh data matters more than continuity

    Pushing down means cloning your live site onto staging. You overwrite whatever’s currently in staging with a fresh snapshot of production. This is the default behavior for most hosts—and it’s the right move when you’re testing something that depends on current content or user data.

    Use push-down sync when:

    • You’re testing a new analytics plugin and need real post data to verify tracking
    • Your live site has grown significantly since the last clone (new posts, pages, or taxonomy terms)
    • You’re debugging a performance issue that only appears with production-scale database size
    • You need to test a membership plugin against actual user roles and subscription states

    The tradeoff: anything you built or configured in staging gets erased. If you spent two days setting up a new WooCommerce flow in staging, then pushed production down to test a different plugin, that work is gone.

    Most hosts don’t warn you. BigScoots’s staging tool does show a confirmation dialog, but it’s easy to click through without reading.

    Pulling up: when you’ve built something new in staging

    Pulling up means pushing your staging environment to production. You overwrite the live site with whatever you built in the sandbox. This is how you deploy new features, redesigns, or structural changes you’ve tested in isolation.

    Use pull-up sync when:

    • You’ve built a new landing page or section in staging and want it live
    • You’ve reconfigured permalink structures, custom post types, or taxonomies
    • You’ve tested a major plugin update (e.g., WooCommerce, Elementor) and want to deploy it
    • You’ve migrated content from another CMS into staging and validated the import

    The danger: you’ll overwrite any content published on the live site since the last clone. If your staging environment is three days old and you pull it up, any posts, comments, form entries, or orders created in those three days vanish.

    This is catastrophic for active sites. If you publish daily or run e-commerce, you can’t safely pull staging up without losing data unless you’ve frozen live updates.

    Selective sync: the middle path most hosts don’t offer

    The ideal workflow is selective sync—pushing only files (themes, plugins, uploads) or only database tables (posts, settings, users) in either direction. Most one-click staging tools don’t support this. They’re all-or-nothing.

    If you need selective sync, you have two options:

    Manual export/import: Use a plugin like WP Migrate DB to export specific tables from staging, then import them into production. You can exclude tables (e.g., skip wp_posts to avoid overwriting live content, but import wp_options to deploy new settings). This takes longer but gives you control.

    File-level deployment: If you’re only changing theme files or custom plugins, skip the staging push entirely. Use SFTP or Git to deploy files directly to production, leaving the database untouched. This is faster and safer for code-only changes, but requires you to track which files changed.

    The sync direction checklist

    Before you click the sync button, ask:

    • Has live content changed since I cloned staging? (Check post count, user count, or recent order IDs.)
    • Did I build anything in staging I need to keep? (New pages, plugin configs, theme customizations.)
    • Am I testing something that needs fresh data, or deploying something I built?
    • Can I afford to lose three days of live content, or three days of staging work?

    If the answer to both “Has live content changed?” and “Did I build anything in staging?” is yes, you’re in a conflict state. Don’t sync in either direction. Export what you built in staging manually, push production down to refresh staging, then re-import your changes and test again before pulling up.

    Most staging failures aren’t technical. They’re directional. The button works fine—it just does exactly what you told it to, in the wrong direction, at the wrong time.

    What’s your staging workflow? Reply with how you handle sync direction—I’m collecting operator setups for a follow-up piece on staging best practices.

  • WordPress multisite user roles: when admin doesn’t mean super admin

    WordPress multisite user roles: when admin doesn’t mean super admin

    WordPress multisite user roles: when admin doesn't mean super admin
    Photo: Matinbeigi via Wikimedia Commons (CC BY-SA 4.0)

    WordPress multisite introduces a permissions layer most solo operators don’t expect: the distinction between a site administrator and a network super admin. If you’ve ever granted someone “admin” access and watched them fail to install plugins, or wondered why a client can’t manage billing when they “own” their site, this is why.

    The role split isn’t a bug. It’s architectural. But it catches people off guard because single-site WordPress collapses these into one role, and most multisite documentation assumes you’re running a university network, not a cluster of client sites or a portfolio of properties you manage alone.

    What each role actually controls

    A site administrator has full control over content, users, themes, and settings within a single site on the network. They can publish posts, moderate comments, manage menus, assign roles to other users on that site, and switch between themes—if you’ve allowed theme switching at the network level.

    They cannot install or activate plugins, install new themes, edit PHP files via the theme editor (even if it’s enabled), access the network admin dashboard, or create new sites. Those permissions belong exclusively to the super admin.

    A super admin controls the entire network. They can create and delete sites, install and network-activate plugins, upload themes, manage network-wide settings, and promote or demote users across all sites. They also inherit site-level admin privileges on every site in the network, whether explicitly assigned or not.

    If you’re running a multisite as a solo operator—maybe you manage five niche content sites under one installation, or you host client projects—you’re almost certainly the only super admin. Everyone else, including clients who “own” their site, is a site administrator at best.

    When role confusion breaks workflows

    The most common issue: a client or collaborator reports they can’t install a plugin you told them to use. You check their account, see “Administrator” next to their name, and assume the platform is broken. It’s not. They’re a site admin, not a super admin, and multisite doesn’t let site admins touch plugins by default.

    The second surprise: user management. A site administrator can add users and assign roles on their site, but they can’t remove a user from the network entirely. If someone needs to be deleted—not just demoted or removed from one site—that’s a super admin task. This creates cleanup debt if you don’t audit regularly.

    The third trap: billing and domain mapping. If you’re using a plugin like Mercator or a host-managed domain mapping tool, site admins often can’t change the primary domain, even if they “own” the site contractually. That permission usually requires super admin access or a custom capability you’ve explicitly granted.

    How to audit and delegate access correctly

    Start by listing every user with super admin privileges. In the network admin dashboard, go to Users → Super Admins. If you see names you don’t recognise, or former contractors still listed, remove them immediately. Super admin is an all-or-nothing role; there’s no way to grant partial network control without a custom plugin.

    For site-level access, go to each site’s dashboard and review Users → All Users. Confirm that client administrators are scoped to their site only. If someone needs plugin installation rights but shouldn’t control the whole network, you have three options:

    • Pre-install and network-activate the plugins they need, then let them configure settings as a site admin
    • Use a plugin like Multisite Plugin Manager or User Role Editor to grant granular capabilities—like install_plugins—to site admins on a per-site basis
    • Promote them to super admin temporarily, have them complete the task, then demote them (risky, not recommended for clients)

    Most operators choose option one. It’s cleaner and avoids the support overhead of explaining why a client can activate a plugin but not delete it from the network.

    One non-obvious behaviour: user registration on multisite

    If you enable open registration at the network level (Network Admin → Settings → Allow new registrations), users who sign up are added to the network, not automatically to any specific site. They exist in the user table but have no role and no dashboard access until a site admin or super admin assigns them to a site.

    This confuses operators who expect self-service membership sites. A user registers, receives a confirmation email, logs in, and sees a blank dashboard with no menu items. They’re authenticated but not authorised anywhere. You have to assign them to a site and give them a role—Subscriber, Contributor, whatever fits—before they see content or functionality.

    If you’re running a paid community or course platform on multisite, automate this with a membership plugin that handles role assignment on purchase. Don’t rely on WordPress’s default registration flow.

    Multisite user roles aren’t complicated once you internalise the two-tier structure. But if you’re migrating from single-site WordPress or onboarding a client who expects full control, set expectations early. “Administrator” means something different here, and assuming otherwise costs you support time you don’t have.

    One Two Three Send publishes operator-focused breakdowns like this one every day. Subscribe to catch the next deep dive on the tools and infrastructure that actually matter.

  • WordPress transactional email: when wp_mail() fails silently

    WordPress transactional email: when wp_mail() fails silently

    WordPress transactional email: when wp_mail() fails silently
    Photo by Mariia Shalabaieva on Unsplash

    WordPress sends password resets, comment notifications, and form submissions through a single PHP function: wp_mail(). It works—until it doesn’t. And when it breaks, you usually won’t know.

    There’s no delivery confirmation, no bounce handling, no retry logic. The function returns true if it handed the message to your server’s mail transport. What happens after that is invisible.

    For solo operators running contact forms, membership sites, or order confirmations, silent email failure costs conversions and creates support overhead. Here’s when the built-in system breaks, what to replace it with, and how to make the switch without touching every plugin individually.

    When wp_mail() actually breaks

    WordPress uses your server’s local mail transfer agent by default—usually Sendmail or Postfix. Shared hosting providers throttle or block outbound SMTP to prevent spam. Budget VPS instances often ship with no MTA configured at all.

    Even when mail leaves your server, deliverability suffers. Your domain lacks proper SPF and DKIM records for server-originated mail. Gmail and Outlook route it to spam or reject it outright. You’ll never see the bounce.

    Common failure scenarios:

    • Password reset emails never arrive, users assume the form is broken
    • WooCommerce order confirmations vanish, customers contact support
    • Gravity Forms or Contact Form 7 submissions disappear after the success message
    • Comment reply notifications stop working, engagement drops

    The function still returns true. WordPress has no idea delivery failed.

    Replace wp_mail() with an SMTP plugin or API bridge

    You need to route WordPress mail through a transactional email service with proper authentication and delivery tracking. Two approaches work:

    SMTP plugins reconfigure wp_mail() to connect to an external mail server. WP Mail SMTP (free) and Post SMTP (free) both support Gmail, SendGrid, Mailgun, and Amazon SES. You add credentials, test a message, and every plugin that calls wp_mail() automatically routes through the new transport.

    Setup takes ten minutes. The catch: SMTP connections can time out under load, and you’re still managing API keys in the WordPress admin.

    API-based plugins replace the SMTP handshake with HTTP calls. Postmark’s official plugin (free) and Mailgun’s plugin both inject an API bridge. Delivery is faster, retries are automatic, and you get real bounce logs in the service dashboard.

    For solo operators sending under 1,000 transactional emails per month, Postmark’s free tier covers password resets and form notifications. Once you cross 1,000 sends, pricing starts at $10/month for 10,000 emails—still cheaper than the support time spent debugging silent failures.

    One non-obvious configuration detail

    After you install an SMTP or API plugin, set a dedicated “From” address and verify it with your email service. Most plugins default to [email protected], which triggers spam filters if that address doesn’t exist or lacks DNS records.

    Create [email protected] or [email protected] as a real mailbox (or alias), add it to your transactional service’s verified sender list, and configure the plugin to use it. This single step fixes 80% of residual deliverability problems after switching away from wp_mail().

    Also: enable logging in the plugin settings. WP Mail SMTP and Post SMTP both include email logs that show exactly what fired, when, and to whom. When a customer says they didn’t get a receipt, you’ll know whether it was sent.

    When to stay with wp_mail()

    If you’re running a single-author blog with comments disabled and no forms, the built-in function is fine. You’re only sending password resets to yourself, and you’ll notice if those break.

    But the moment you add a contact form, membership plugin, e-commerce checkout, or any user-triggered email, the risk shifts. Silent failure becomes a business problem, not a technical curiosity.

    Switching to a transactional service doesn’t require a developer. Install a plugin, add an API key, send a test. If the test arrives and your logs confirm it, you’re done.

    What’s breaking on your WordPress site right now? Hit reply and tell us which email mystery you’ve been ignoring. We’ll cover it in a future piece.

  • WordPress object caching: when to enable Redis and when memcached wins

    WordPress object caching: when to enable Redis and when memcached wins

    WordPress object caching: when to enable Redis and when memcached wins
    Photo by Fikret tozak on Unsplash

    Most WordPress performance guides tell you to “enable object caching” without explaining which engine to use or what you’re actually optimizing for. Redis and memcached both cache database query results in memory, but they handle data persistence, eviction policies, and scale differently—and picking the wrong one can waste server resources or create new bottlenecks.

    Here’s how object caching actually works in WordPress, when each engine makes sense, and one non-obvious configuration detail that changes performance more than the engine choice itself.

    What object caching does in WordPress

    WordPress makes dozens of database queries per page load: post metadata, user permissions, theme options, widget settings. Object caching stores the results of those queries in RAM so subsequent requests skip the database entirely.

    Without object caching, every page view hits MySQL. With it enabled, WordPress checks the cache first. If the data exists and hasn’t expired, the query never runs. This matters most on sites with high traffic, complex queries, or shared hosting where database connections are rate-limited.

    The cache doesn’t replace page caching (which stores rendered HTML) or CDN edge caching (which serves static assets). It sits between WordPress and the database, cutting query volume by 40–80% on typical sites.

    Redis vs. memcached: the actual differences

    Both engines store key-value pairs in memory. The differences show up in three areas:

    Data persistence. Redis can write cache data to disk periodically, so a server restart doesn’t lose everything. Memcached stores data only in RAM—reboot the server, lose the cache. For most WordPress sites, this doesn’t matter; cache rebuilds in minutes. But if you’re caching computationally expensive data (API responses, complex WooCommerce queries), Redis persistence reduces rebuild load.

    Data structures. Redis supports lists, sets, sorted sets, and hashes. Memcached handles strings only. WordPress core uses simple key-value storage, so this rarely matters unless a plugin (like some analytics or membership tools) needs complex queries. WooCommerce session handling, for example, works better with Redis because it stores cart data as hashes.

    Eviction policies. Memcached uses LRU (least recently used) eviction: when memory fills, it drops the oldest unused items. Redis offers multiple policies (LRU, LFU, random, TTL-based). For WordPress, LRU works fine in both. The policy matters less than setting the right memory limit.

    Performance-wise, memcached is marginally faster for pure key-value lookups—10–15% in benchmarks. Redis trades that speed for flexibility. On a typical WordPress site serving 50,000 monthly visitors, you won’t notice the difference.

    When to use each engine

    Use memcached if: You’re running a straightforward content site (blog, magazine, newsletter archive), your hosting provider offers it pre-configured, or you want the simplest possible setup. Managed WordPress hosts like BigScoots often provision memcached by default because it’s lightweight and requires minimal tuning.

    Use Redis if: You run WooCommerce, a membership site, or any plugin that benefits from persistent sessions; you need cache data to survive server restarts; or you’re already using Redis for something else (job queues, rate limiting) and want to consolidate.

    If your host offers both and you’re unsure, start with memcached. It’s easier to configure and harder to misconfigure.

    The non-obvious detail: connection method matters more than engine choice

    Most WordPress object caching plugins default to TCP socket connections (127.0.0.1:6379 for Redis, 127.0.0.1:11211 for memcached). Switching to Unix sockets cuts latency by 20–40% because the connection skips network stack overhead—even on localhost.

    To use Unix sockets, your cache engine and WordPress plugin both need reconfiguration. For Redis with the Redis Object Cache plugin, edit wp-config.php:

    define('WP_REDIS_SCHEME', 'unix');
    define('WP_REDIS_PATH', '/var/run/redis/redis.sock');

    For memcached with the Memcached Object Cache plugin:

    $memcached_servers = array(
      'unix:///var/run/memcached/memcached.sock'
    );

    Your hosting provider needs to enable socket support—most managed hosts do, but you may need to open a support ticket. The socket path varies by server (check /var/run/ or ask support).

    On a test site serving 5,000 daily visitors, switching from TCP to Unix sockets dropped average query time from 18ms to 11ms. That’s more impact than switching from memcached to Redis.

    How to tell if it’s working

    Enable query monitoring with the Query Monitor plugin. Before object caching, you’ll see 40–80 database queries per page load. After enabling and warming the cache (visit a few pages), that should drop to 15–30.

    If query count doesn’t drop, check three things: cache isn’t connected (plugin shows “Disconnected” in settings), cache memory is too small (check maxmemory setting—allocate at least 128 MB), or your theme/plugins bypass the object cache with direct SQL queries (rare, but some poorly-coded tools do this).

    Object caching won’t fix slow page rendering or large image files—it only cuts database load. Pair it with page caching (WP Rocket, LiteSpeed) and a CDN for full effect.

    Running a high-traffic WordPress site? Subscribe to One Two Three Send for weekly deep-dives on hosting, performance, and infrastructure decisions that actually move the needle.

  • WordPress post revisions database bloat: when to cap and purge

    WordPress post revisions database bloat: when to cap and purge

    WordPress post revisions database bloat: when to cap and purge
    Photo: Simone Bignetti via Wikimedia Commons (CC BY-SA 4.0)

    WordPress saves every draft change you make as a post revision. Every autosave, every manual save, every time you click update. Those revisions live forever in the wp_posts table unless you explicitly limit or purge them.

    For a solo operator publishing twice a week, this rarely matters. For a site with daily posts, guest contributors, or heavy editing cycles, you can end up with tens of thousands of revision rows that slow down queries, inflate backups, and make database migrations painfully slow.

    Here’s when revisions become a problem, how to cap them, and what happens when you purge old ones.

    How revisions accumulate faster than you think

    WordPress creates a new revision on every save—manual or automatic. The default autosave interval is 60 seconds. If you spend 20 minutes editing a post and save manually three times, you’ve just created roughly 23 revisions for a single post.

    Multiply that across a year of publishing, especially if you have multiple authors or use tools that auto-update posts (like content refreshers or dynamic blocks), and you’re looking at 5,000–15,000 revision rows per year for a modest site.

    Each revision is a full duplicate of the post content at that moment—title, body, meta. It’s not a diff. That’s why a site with 500 published posts can have 8,000 rows in wp_posts.

    When revisions start causing real problems

    Revisions don’t directly slow down your front end—visitors never query them. But they do three things that hurt:

    • Admin dashboard queries slow down. The post editor loads all revisions for the current post. If a single post has 200 revisions, that’s a noticeable lag when you open it.
    • Backup file sizes balloon. Your nightly database backup includes every revision. A 50 MB database becomes 200 MB, which slows down both the backup process and restores.
    • Database exports and migrations take longer. If you’re moving hosts or cloning a site, you’re waiting for thousands of unnecessary rows to export and import.

    You’ll notice the impact most clearly when running a plugin like WP-Optimize or querying the database directly. A SELECT that should return 500 posts instead scans 8,000 rows because revisions share the same table.

    How to cap revisions going forward

    Add this line to your wp-config.php file, anywhere above the line that says /* That's all, stop editing! */:

    define( 'WP_POST_REVISIONS', 5 );

    This limits WordPress to keeping the five most recent revisions per post. Older revisions are automatically deleted when a new one is created.

    Five is a sensible default for most solo operators. It’s enough to undo a bad edit or recover from an accidental overwrite, but not so many that you’re hoarding years of draft history you’ll never look at.

    If you want to disable revisions entirely, set it to false:

    define( 'WP_POST_REVISIONS', false );

    This stops all future revisions. Autosave still works—you can still recover unsaved changes—but WordPress won’t store a permanent history.

    How to purge existing revisions

    Capping revisions only affects new saves. It doesn’t touch the thousands of old revisions already sitting in your database.

    To remove those, you have two options: a plugin or a direct SQL query.

    Plugin method: Install WP-Optimize (free) or WP Sweep (free). Both have a one-click option to delete all post revisions. WP-Optimize also lets you schedule automatic cleanups weekly or monthly.

    SQL method: If you’re comfortable with database access, run this query in phpMyAdmin or your host’s database tool:

    DELETE FROM wp_posts WHERE post_type = 'revision';

    This deletes every revision from your database immediately. Make sure you have a backup first—this is irreversible.

    After running the query, also run OPTIMIZE TABLE wp_posts; to reclaim the disk space and rebuild the table index. Without this step, your database file size won’t actually shrink.

    The tradeoff: losing granular undo history

    Capping or purging revisions means you can’t travel back to a draft from six months ago. For most operators, that’s fine—you’re not auditing edit history or rolling back to ancient versions.

    But if you’re running a site with compliance requirements, guest contributors who might dispute changes, or content that frequently gets reverted (like policy pages or legal disclaimers), keep more revisions or use a version-control plugin like Revisionary or WP Document Revisions instead.

    For everyone else, five revisions and an annual purge is a reasonable maintenance habit that keeps your database lean without sacrificing practical undo capabilities.

    If you’re dealing with WordPress performance issues beyond revisions—caching, plugin conflicts, or database optimisation—reply and let me know what’s slowing you down. I’ll cover it in a future piece.

  • WordPress multisite subdomain DNS: how wildcard A records actually work

    WordPress multisite subdomain DNS: how wildcard A records actually work

    WordPress multisite subdomain DNS: how wildcard A records actually work
    Photo by Markus Spiske on Unsplash

    WordPress multisite with subdomains sounds elegant in theory: every new site gets sitename.yourdomain.com instead of yourdomain.com/sitename. But the DNS configuration required to make that work trips up even experienced operators, especially when a CDN sits in front.

    If you’ve ever added a new multisite subsite and found it unreachable for hours—or had some subsites resolve while others don’t—the issue is almost always in how your wildcard A record is configured, cached, or overridden.

    What wildcard DNS actually does

    A wildcard A record in your DNS zone looks like this:

    *.yourdomain.com. A 192.0.2.10

    That asterisk tells the nameserver: “for any subdomain that doesn’t have an explicit record, send traffic to this IP.” So when someone visits newsite.yourdomain.com, and you haven’t manually created a DNS entry for newsite, the wildcard catches it and routes it to your server.

    WordPress multisite then inspects the HTTP Host header, matches it to a subsite in the database, and serves the right content. The wildcard doesn’t create the site—it just ensures the request reaches your server.

    But here’s the catch: wildcard records have lower priority than explicit records. If you’ve previously created an A record for staging.yourdomain.com pointing elsewhere, the wildcard won’t override it. That explicit record wins.

    When propagation stalls and caching hides the issue

    DNS records have a Time-To-Live (TTL) value, usually between 300 seconds (5 minutes) and 86400 seconds (24 hours). When you add or change a wildcard A record, recursive resolvers—like those run by ISPs or Cloudflare’s 1.1.1.1—cache the old state until the TTL expires.

    If your previous wildcard record had a 24-hour TTL and you just changed the IP, some visitors won’t see the update for a full day. Your local machine might resolve correctly (because your DNS client happened to refresh), but your phone on cellular data still hits the old IP.

    This creates a debugging mirage: the site works for you, fails for others, and you can’t reproduce the issue. The fix is to lower your wildcard A record TTL to 300 seconds before making changes, wait for the old TTL to expire, then make the change. After it propagates, you can raise the TTL again if you want.

    The CDN wildcard gotcha

    If you’re using a CDN like Cloudflare, BunnyCDN, or Fastly, the wildcard DNS record alone isn’t enough. Most CDNs require you to explicitly enable wildcard proxying or add each subdomain to a whitelist.

    On Cloudflare, for example, wildcard DNS records (*.yourdomain.com) can be proxied (orange cloud), but only on Business or Enterprise plans. On Free and Pro plans, the wildcard record must be DNS-only (grey cloud). That means requests hit your origin server directly—no CDN caching, no DDoS protection, no automatic SSL for new subsites.

    If you’re on a CDN plan that doesn’t support wildcard proxying, you have two options:

    • Upgrade the CDN plan to enable wildcard SSL and proxying.
    • Pre-create DNS records for each subsite manually and proxy those individually. This works, but it defeats the automation benefit of multisite subdomains.

    BunnyCDN supports wildcard pull zones on all plans, but you need to configure the hostname pattern in the pull zone settings. If you skip that step, the CDN won’t know which origin to pull from, and requests fail with a 404 at the edge.

    Non-obvious tip: test with dig, not a browser

    When you’re debugging wildcard DNS, don’t rely on your browser. Browsers cache DNS internally, and your OS has its own resolver cache. Instead, use dig or nslookup to query the authoritative nameserver directly:

    dig @ns1.yourprovider.com randomsubdomain.yourdomain.com

    Replace ns1.yourprovider.com with one of your domain’s authoritative nameservers (find them with dig yourdomain.com NS). If the wildcard is configured correctly, you’ll see an A record response with your origin IP, even for a subdomain that doesn’t exist in WordPress yet.

    If dig returns NXDOMAIN, the wildcard isn’t live yet—either it hasn’t propagated, or it wasn’t saved correctly in your DNS provider’s dashboard.

    When to use multisite subdomains vs. subdirectories

    Subdomain multisite makes sense when you’re running distinct properties under one WordPress install—think a network of regional sites, or client sites you manage centrally. But if you’re a solo operator running one content brand, subdirectories (yourdomain.com/blog, yourdomain.com/shop) are simpler. You avoid DNS complexity, CDN wildcard restrictions, and SSL certificate headaches.

    Subdomain multisite also complicates migration. Moving to a new host means updating the wildcard A record and waiting for propagation. With subdirectories, you update one A record and you’re done.

    If you’re already committed to subdomain multisite and hitting DNS or CDN issues, the fix is usually one of three things: lower TTL, enable wildcard proxying on your CDN, or confirm the wildcard A record actually saved. Test with dig, not guesswork.

    Got a WordPress hosting or DNS question? Reply to this email—I cover one reader question every Sunday.

  • WordPress plugin conflict logs: where errors hide and how to read them

    WordPress plugin conflict logs: where errors hide and how to read them

    WordPress plugin conflict logs: where errors hide and how to read them
    Photo by Brett Jordan on Unsplash

    When a WordPress plugin breaks your site, you usually get a white screen, a cryptic error message, or—worse—nothing at all. The checkout button stops working. The contact form silently drops submissions. A scheduled post never publishes.

    Most solo operators disable plugins one by one until something works again. That approach finds the culprit, but it doesn’t tell you why the conflict happened or whether it’ll break again after the next update.

    WordPress writes conflict and error data to several places. If you know where to look and what patterns matter, you can diagnose plugin conflicts in minutes instead of hours, and you’ll catch silent failures before readers do.

    Where WordPress writes plugin error logs

    WordPress doesn’t enable debug logging by default. When a plugin conflict occurs, the error might surface in your browser, but more often it gets written to a server log that most operators never open.

    There are three places to check:

    • debug.log in /wp-content/ — only appears if WP_DEBUG_LOG is enabled in wp-config.php. This is the primary conflict log. It captures PHP errors, warnings, notices, and deprecated function calls. Most plugin conflicts show up here first.
    • Server error logs — your hosting provider writes PHP fatal errors and web server errors here. Location varies: cPanel puts them in /logs/, managed WordPress hosts expose them in dashboards. BigScoots surfaces them in the control panel under “Error Log.” Fatal errors that kill the site bypass debug.log and land here.
    • Browser console — JavaScript conflicts don’t write to PHP logs. Open your browser’s developer tools (F12) and check the Console tab. A missing .js file, a jQuery version mismatch, or a React component error will show here, not in WordPress logs.

    If WP_DEBUG_LOG isn’t enabled, add this to wp-config.php just above the line that says /* That's all, stop editing! */:

    define('WP_DEBUG', true);
    define('WP_DEBUG_LOG', true);
    define('WP_DEBUG_DISPLAY', false);

    This writes errors to debug.log without displaying them to site visitors. Leave it on for a week. If a conflict exists, it’ll log.

    What plugin conflict entries actually look like

    A typical conflict log entry looks like this:

    [23-Jul-2026 14:32:18 UTC] PHP Fatal error: Uncaught Error: Call to undefined function wp_mail() in /home/user/public_html/wp-content/plugins/custom-contact-form/contact.php:47

    Here’s what matters:

    • Timestamp — when the error fired. Cross-reference this with when you updated a plugin or when a visitor reported a problem.
    • Error type — Fatal errors break execution. Warnings and notices don’t stop the site, but they signal a conflict that might escalate after the next plugin update.
    • File path — tells you which plugin triggered the error. In this case, custom-contact-form is the culprit.
    • Function namewp_mail() is undefined, which usually means the plugin loaded too early, before WordPress core functions were available.

    Not all conflicts are this clear. Sometimes you’ll see dozens of deprecation notices from a single plugin. That’s noise. Look for fatal errors and repeated warnings tied to the same plugin and function. Those are the patterns that predict breakage.

    Tracing breaking changes after plugin updates

    If your site breaks immediately after a plugin update, the log will show the new error. But if the conflict is silent—forms stop submitting, emails stop sending, scheduled posts don’t publish—you need to correlate log timestamps with plugin update times.

    WordPress logs plugin updates in the database, but doesn’t surface them in a readable timeline. If you suspect a recent update caused a conflict, check debug.log for the first error timestamp, then compare it to your plugin update history in the WordPress admin under Dashboard → Updates. That page shows recent updates, but only for the last few days.

    For longer history, query the database directly or use a plugin like WP Activity Log, which tracks every plugin activation, update, and deactivation with timestamps. Cross-reference the log with your debug.log entries. If the first error appeared within an hour of a plugin update, that’s your conflict source.

    What to do once you’ve identified the conflict

    Once you know which plugin is logging errors, you have three options:

    • Roll back the plugin — most managed WordPress hosts offer one-click plugin rollback. If yours doesn’t, download the previous version from the WordPress plugin repository’s “Advanced View” tab and upload it manually. This buys time while you wait for a patch.
    • Disable the conflicting feature — some plugins let you toggle features. If the conflict stems from a feature you don’t use, turn it off instead of disabling the entire plugin.
    • Contact the developer — if the plugin is actively maintained, report the conflict with your log excerpt, WordPress version, PHP version, and conflicting plugin name. Most developers fix conflicts within a few days if you give them clean reproduction steps.

    Don’t leave WP_DEBUG enabled indefinitely on a live site. Once you’ve identified and resolved the conflict, set WP_DEBUG and WP_DEBUG_LOG back to false. Debug logs grow large and can expose server paths to anyone who guesses the debug.log URL.

    One non-obvious tip: check for JavaScript conflicts separately

    PHP logs won’t capture JavaScript conflicts. If a plugin breaks your site’s front-end interactivity—buttons stop responding, modals don’t open, checkout forms freeze—open your browser console and look for red error messages. Common culprits: two plugins loading different versions of jQuery, or a plugin enqueueing a script that depends on another script that didn’t load.

    To confirm a JavaScript conflict, disable plugins one by one while keeping the browser console open. When the red errors disappear, you’ve found the plugin responsible.

    Want more WordPress infrastructure breakdowns like this? Reply with the hosting or plugin behaviour you want explained next—we read every response and route the best questions into future issues.

  • WordPress post scheduler cron: how it works and when it fails

    WordPress post scheduling feels like magic—until a post doesn’t publish on time. You set a future date, hit schedule, and assume the post will go live at 9:00 AM sharp. Sometimes it does. Sometimes it publishes three minutes late. Sometimes it doesn’t publish at all until you manually refresh the site.

    The reason is simple: WordPress doesn’t use real cron. It fakes it. And for solo operators running lean sites with inconsistent traffic, that fake cron system breaks more often than you’d expect.

    How WordPress scheduling actually works

    When you schedule a post, WordPress stores the future publish time in the database and registers a “cron event” tied to that timestamp. But WordPress cron isn’t a server-level scheduled task—it’s a PHP script that runs only when someone visits your site.

    Every time a page loads, WordPress checks if any cron events are overdue. If one is, it spawns a background HTTP request to wp-cron.php, which processes the event queue. That queue includes scheduled posts, plugin tasks, update checks, and anything else hooked into the cron system.

    This approach works fine for high-traffic sites. If you’re getting page views every few seconds, cron events fire close to their scheduled time. But if your site gets sporadic traffic—common for new operators, niche blogs, or B2B content sites—you might not get a visitor at 9:00 AM. The post sits in the queue until someone (or something) hits the site.

    When scheduling breaks

    Three common failure modes:

    Low traffic delays publication. If your site averages ten visitors per hour and you schedule a post for 6:00 AM, it might not publish until 6:43 AM when the first human visitor triggers wp-cron.php. Search engines and RSS readers may already have crawled your site and missed it.

    Caching plugins disable wp-cron.php. Some full-page caching setups (especially aggressive CDN configs or static site generators bolted onto WordPress) block the background HTTP request to wp-cron.php. The page load completes, but the cron event never fires. Posts stay in “scheduled” status indefinitely.

    The cron queue gets clogged. If a plugin registers dozens of cron events—backup scripts, API syncs, email queue processors—and one of those tasks hangs, the entire queue stalls. WordPress processes cron events sequentially in a single request. A 30-second timeout on one event blocks everything behind it, including your scheduled post.

    How to fix it

    The cleanest solution: disable WordPress’s fake cron and use real server-level cron instead.

    Add this line to wp-config.php:

    define('DISABLE_WP_CRON', true);

    Then add a real cron job via your hosting control panel or SSH. Most hosts (including BigScoots, SiteGround, and Kinsta) let you add cron jobs through cPanel or a custom dashboard. Set it to run every 5–15 minutes:

    */15 * * * * wget -q -O - https://yoursite.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1

    Or use curl if wget isn’t available:

    */15 * * * * curl -s https://yoursite.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1

    Now cron events fire on schedule, regardless of traffic. Posts publish within 15 minutes of their target time (or within 5 minutes if you set the interval tighter). RSS readers and search crawlers see content when you intended.

    One non-obvious benefit: this setup also makes plugin-based automations more reliable. If you’re using WordPress to queue emails, sync data to external APIs, or run nightly cleanup tasks, real cron ensures those jobs complete even when your site is quiet.

    One thing to watch

    If you run real cron and have high traffic, you might end up triggering wp-cron.php twice in the same minute—once from the server cron job, once from a visitor’s page load. This usually isn’t a problem (WordPress locks cron execution to prevent duplicate runs), but if you’re obsessive about server load, keep DISABLE_WP_CRON enabled and let the server-level job handle everything.

    If you’d rather not touch server config, a few managed WordPress hosts (Kinsta, WP Engine) run real cron by default. Check your host’s documentation—some silently replace wp-cron.php without telling you.

    Want more infrastructure breakdowns like this? Subscribe to One Two Three Send—we explain the invisible plumbing that makes (or breaks) online businesses.