Author: onetwothreeadmin

  • Scheduled post APIs fail silently—here’s what gets dropped

    Scheduled post APIs fail silently—here’s what gets dropped

    Scheduled post APIs fail silently—here's what gets dropped
    Photo by David Pupăză on Unsplash

    Scheduled posts disappear more often than you think. Not because you misconfigured the time zone or forgot to hit publish—because the API between your scheduling tool and the destination platform failed, and nothing told you.

    If you’re running a content business that depends on scheduled social posts, WordPress post queues, or automated newsletter sends, you’ve probably experienced this: a post that was queued for 9 AM simply never appeared. No error email. No dashboard alert. Just silence.

    Here’s what actually breaks, and how to catch it before your audience notices.

    Why scheduled post APIs fail

    Most scheduling tools—whether it’s Buffer, Publer, CoSchedule, or WordPress’s native post scheduler with a third-party plugin—rely on API calls to the destination platform. Those calls can fail for three common reasons:

    API rate limits. Twitter, LinkedIn, and Facebook all enforce per-hour or per-day post limits. If your account or app hits that ceiling, subsequent requests get rejected. Some tools queue the retry; most just drop it.

    Expired access tokens. OAuth tokens that connect your scheduler to Instagram, LinkedIn, or YouTube expire after 60–90 days depending on the platform. If the tool doesn’t refresh the token automatically—or if the refresh fails—your post never leaves the queue.

    Webhook timeouts. WordPress post schedulers that rely on WP-Cron or external services like EasyCron depend on HTTP requests firing at the right time. If your server is under load, or the webhook times out, the post stays in “scheduled” status indefinitely.

    None of these scenarios generate user-facing errors by default. The tool logs the failure internally, but you don’t see it unless you check the logs—and most solo operators don’t.

    What actually gets dropped

    The content most likely to disappear:

    Social posts scheduled in bulk. If you queue 20 posts at once and token #14 expires mid-batch, posts 15–20 never publish. The tool may show them as “sent” in the UI because the request was attempted, not because it succeeded.

    WordPress posts with complex taxonomies. If your scheduled post includes custom fields, featured images hosted on an external CDN, or category assignments that depend on another plugin, and any of those dependencies fail to load at publish time, WordPress either publishes a broken version or silently reschedules it.

    Newsletter sends via third-party integrations. If you schedule a newsletter send through Zapier or Make, and the ESP’s API returns a 429 (rate limit) or 401 (auth error), the automation may not retry. Your send just… doesn’t happen.

    How to catch failures before your audience does

    Set up three checks:

    Daily log review. Most scheduling tools bury error logs in settings or account dashboards. Set a recurring calendar event to check them. Look for HTTP 4xx or 5xx codes, token expiration warnings, or “retry failed” entries. If you’re using Publer, the activity log shows per-post status codes. For WordPress, the WP Crontrol plugin exposes missed or failed cron events.

    Automated monitor for published content. Use an RSS monitor like Feedly or an uptime tool like Better Uptime to ping your site’s feed or social profile every hour. If a scheduled post doesn’t appear in the feed within 15 minutes of its scheduled time, you get an alert. This won’t tell you why it failed, but it will tell you that it failed.

    Redundant token refresh. For tools that use OAuth, manually refresh tokens once a month even if the tool says they’re valid. Most platforms let you revoke and re-authorise without losing post history. This preempts 90% of silent token expiration failures.

    When to stop scheduling and publish manually

    If you’re publishing fewer than five posts a week across all channels, the operational overhead of monitoring scheduled post APIs often exceeds the time saved. Manual publishing takes 60 seconds per post. Debugging a silent API failure, reconstructing what didn’t publish, and re-queuing it takes 20 minutes.

    The break-even point: if you’re scheduling more than 25 posts a month, automation saves time. Below that, the risk of silent failure isn’t worth it unless you’ve built the monitoring workflow described above.

    One exception: if your content is time-sensitive—launch announcements, event coverage, earnings commentary—always publish manually or use a tool with guaranteed delivery SLAs and real-time alerting. Most don’t offer that.

    Read more like this

    If you found this useful, subscribe to One Two Three Send—we publish operator-focused breakdowns like this one every day. No fluff, no beginner listicles, just the technical details that matter when you’re running a content business solo.

    Heads up — some links in this article are affiliate links. If you sign up through them, we may earn a small commission at no extra cost to you. We only recommend tools we use ourselves.

  • Traffic attribution breaks when UTM parameters get stripped by platforms

    Traffic attribution breaks when UTM parameters get stripped by platforms

    Traffic attribution breaks when UTM parameters get stripped by platforms
    Photo by Tim Mossholder on Unsplash

    You tag every link with UTM parameters. You check Google Analytics. Half your traffic shows up as “direct” even though you know it came from email or social.

    The problem isn’t your tagging discipline—it’s that platforms strip UTM parameters before the user ever reaches your site. Email clients do it for privacy. Social apps do it to hide referral data. Link shorteners do it by accident when they redirect through multiple hops.

    If you’re running a content business and relying on UTM tags to measure what’s working, you need to know what actually survives the trip from send to click.

    What strips UTM parameters and when

    Email clients are the worst offenders. Apple Mail Privacy Protection, Gmail’s link scanner, and Outlook’s Safe Links feature all rewrite URLs before a human clicks them. Sometimes the UTM parameters survive the rewrite. Sometimes they don’t.

    Apple Mail strips them inconsistently—if the recipient opens the email on iOS with tracking protection enabled, parameters often vanish. Gmail’s link scanner preserves them most of the time, but if the email gets forwarded or opened in a third-party client, all bets are off.

    Social platforms strip them deliberately. LinkedIn removes UTM tags from outbound links in posts (not ads) to prevent attribution leakage to competitors. Twitter used to preserve them but now strips utm_source and utm_medium on mobile app clicks. Facebook Messenger rewrites URLs entirely and drops everything after the ? unless you’re using a Facebook pixel.

    Link shorteners compound the issue. Bit.ly and TinyURL preserve parameters if you paste the full tagged URL into their interface. But if you use their browser extensions or API without explicitly appending the parameters after shortening, the UTM tags get truncated. Redirect chains—where one short link points to another short link—drop parameters at each hop.

    What this looks like in your analytics

    You send a newsletter with utm_source=newsletter&utm_medium=email&utm_campaign=july tagged on every link. You check Google Analytics 4 the next day. Traffic shows up, but 40% is labeled “direct / none” instead of “newsletter / email.”

    The same thing happens with social posts. You share a link on LinkedIn with full UTM tagging. Analytics shows a spike in traffic, but the source reads “direct” or gets misattributed to Google if someone Googled your brand name after seeing the post.

    This isn’t a tracking bug. It’s parameter stripping in action. The user clicked your link, but the UTM tags didn’t survive the platform’s URL rewrite, privacy scanner, or redirect logic.

    How to track attribution when UTM tags fail

    First option: use campaign-specific landing pages instead of UTM parameters. If you’re promoting a guide, create /guide-linkedin and /guide-newsletter as unique URLs that 301 redirect to the real page. You lose some SEO link equity with redirects, but you gain reliable source tracking even when parameters get stripped.

    Second option: append a custom parameter that platforms ignore. Instead of utm_source, use ref=newsletter or via=linkedin. These aren’t standard tracking parameters, so email clients and social platforms don’t recognize them as privacy risks and leave them alone. You’ll need to configure your analytics tool to read these custom parameters as traffic sources—Google Analytics 4 lets you do this with custom dimensions, and most self-hosted tools like Plausible or Fathom support query parameter tracking out of the box.

    Third option: track at the application layer instead of the URL. If someone clicks through from your newsletter and immediately signs up or downloads something, log the referrer in your database at the moment of conversion. This doesn’t help with anonymous traffic, but it gives you attribution for actions that matter—subscribers, buyers, trial signups.

    Fourth option: accept that some traffic will always be misattributed and focus on channel-level trends instead of click-level precision. If you send a newsletter on Tuesday and see a 300% traffic spike on Tuesday afternoon, you don’t need perfect UTM tracking to know the newsletter worked. Same with social posts—watch for correlated traffic increases within an hour of posting.

    When UTM parameters still work

    Paid ads preserve UTM tags more reliably than organic links. Facebook Ads, Google Ads, and LinkedIn Ads all pass UTM parameters through their tracking pixels without stripping them, because the platforms want attribution data for their own dashboards.

    Direct website embeds work, too. If you link from your own blog post to another page on your site, UTM tags survive because there’s no intermediary platform rewriting the URL.

    SMS links preserve parameters as long as you’re not using a link shortener. Twilio, SimpleTexting, and most SMS platforms send the raw URL without modification.

    The key is knowing which channels strip parameters and planning around them instead of assuming your tagging strategy works everywhere.

    Have a question about tracking, attribution, or analytics for your online business? Reply to this email—we answer reader questions every Sunday.

  • WordPress cache expiration headers: what actually gets cached

    WordPress cache expiration headers: what actually gets cached

    WordPress cache expiration headers: what actually gets cached
    Photo: Gaurav Dhwaj Khadka via Wikimedia Commons (CC BY-SA 4.0)

    Most WordPress operators assume their caching plugin handles everything. It doesn’t. Cache-Control and Expires headers—set at the server or application level—determine what gets stored by browsers, proxies, and CDNs, and for how long. WordPress sets some of these by default. Others depend on your host, your caching plugin, or manual configuration.

    If you’ve ever wondered why a CSS file updates instantly but a hero image sticks around for days, or why logged-in users see stale content, the answer is in the headers. Here’s what actually gets cached, what WordPress controls, and when you need to intervene.

    What WordPress sets by default

    Out of the box, WordPress sends Cache-Control: no-cache, must-revalidate, max-age=0 for most dynamic pages—posts, archives, and any page generated by PHP. This tells browsers and intermediaries not to cache the response without revalidation.

    Static assets—images, CSS, JavaScript uploaded to /wp-content/uploads or enqueued via wp_enqueue_script—typically don’t get cache headers from WordPress itself. Your web server (Apache, Nginx) or host sets them. Most hosts default to one year (max-age=31536000) for images and fonts, and shorter windows (one week to one month) for CSS and JS.

    If you’re on shared hosting without custom server config, you’re stuck with those defaults unless you use a plugin or CDN to override them.

    Where caching plugins take over

    Full-page caching plugins—WP Rocket, W3 Total Cache, LiteSpeed Cache—generate static HTML and serve it with their own headers. Most set Cache-Control: public, max-age=3600 (one hour) or longer for cached pages, and private, no-cache for logged-in users.

    The problem: if your plugin sets a one-hour cache but your CDN or browser already cached the page for 24 hours, the shorter directive won’t matter. Cache layers stack. The longest-lived cache wins until it expires or gets purged.

    Check what’s actually being sent by opening DevTools, loading a page, and inspecting the response headers under the Network tab. Look for Cache-Control, Expires, and Age. If Age is present, the response came from a cache. If it’s missing, it’s a fresh hit.

    When to override the defaults

    Override cache headers when:

    • You version assets manually. If you append ?v=2 or use a build hash in filenames, set a long max-age (one year). The filename or query string will bust the cache when you update.
    • You serve user-specific content. Set Cache-Control: private so CDNs and shared proxies don’t serve one user’s view to another. Cookies usually trigger this automatically, but not always.
    • You publish time-sensitive content. If your site updates every few minutes (live scores, stock tickers, event countdowns), set max-age=60 or lower and pair it with s-maxage for CDN-specific caching.
    • Your CDN and origin disagree. Some CDNs (Cloudflare, for example) respect origin headers by default but let you override them with page rules. If your origin says “cache for one hour” but your CDN caches for 24, you’ll serve stale content unless you configure the CDN to honor the shorter window or purge on publish.

    The non-obvious detail: stale-while-revalidate

    Modern browsers and CDNs support stale-while-revalidate, a directive that serves cached content even after it expires while fetching a fresh copy in the background. If you set Cache-Control: max-age=600, stale-while-revalidate=300, the cache serves the page for 10 minutes, then for another 5 minutes while revalidating. The user never waits, and your server gets fewer simultaneous requests.

    Most WordPress caching plugins don’t expose this setting. You’ll need to add it via your host’s control panel, a custom Nginx/Apache config, or a CDN rule. It’s worth it if you publish irregularly and want to keep pages fast without manual purging.

    How to check what’s actually cached

    Run a quick test:

    • Open an incognito window and load your homepage.
    • Open DevTools → Network → reload the page.
    • Click on the document request (usually the first row) and check the Response Headers.
    • Look for Cache-Control, Expires, Age, and X-Cache (CDN-specific).

    If you see max-age=0 on a static page, your caching plugin isn’t running or isn’t configured for that route. If you see Age: 86400 (24 hours in seconds), the page was cached a day ago and hasn’t been purged.

    Repeat the test logged in. If the headers are identical, you’re serving cached pages to authenticated users—a problem if your site shows user-specific content or admin bars.

    Want more infrastructure breakdowns like this? Subscribe to One Two Three Send for weekly deep-dives into the systems that run online businesses—no fluff, just the details that matter.

  • Substack’s Section feature: when to split your newsletter

    Substack’s Section feature: when to split your newsletter

    Substack's Section feature: when to split your newsletter
    Photo by Markus Winkler on Unsplash

    Substack’s Section feature lets you run multiple newsletters under a single publication. Each Section has its own name, subscription toggle, and archive page—but everything lives under one domain and subscriber dashboard.

    Most operators discover Sections when they want to add a secondary content stream without fragmenting their audience or managing two separate Substacks. The feature works, but only if you understand what it actually controls and what it doesn’t.

    How Sections work

    When you create a Section, you’re adding a category filter to your publication. Subscribers can opt in or out of each Section independently. A reader might subscribe to your main newsletter but skip your weekly link roundup, for example.

    Each post you publish gets assigned to one Section. Your homepage feed shows all posts by default, but readers can filter by Section using the navigation menu. Each Section gets its own RSS feed and archive URL.

    Sections don’t create separate subscriber lists—everyone is still subscribed to your publication. The Section toggle just controls which emails they receive. Your total subscriber count remains unified, and free vs. paid status applies across all Sections.

    This matters for billing. If you have 5,000 subscribers but only 1,000 opted into your premium Section, you’re still paying for 5,000 subscribers. Substack doesn’t prorate based on Section engagement.

    When to use Sections

    Sections make sense when you want to publish different content formats or cadences without forcing every subscriber to receive everything.

    Common use cases: a weekly main newsletter plus a daily news brief; a free newsletter with a paid-only deep-dive Section; a primary topic with a secondary niche that overlaps but doesn’t fully align.

    Sections don’t work well if your content streams target completely different audiences. A marketing newsletter and a cooking newsletter should be separate publications, not Sections. Substack’s discovery and recommendation algorithms treat your publication as a single entity—readers who find you through one Section will see the rest.

    Sections also don’t solve the problem of list fatigue. If subscribers are tuning out, adding more Sections usually makes it worse. You’re better off consolidating or changing your primary content strategy.

    The non-obvious filtering tip

    Substack’s subscriber export includes a sections column that lists which Sections each subscriber has enabled. Most operators ignore this field, but it’s useful for segmentation.

    You can filter your CSV export to find subscribers who opted into one Section but not another. This tells you which content streams resonate and which don’t. If 80% of your subscribers turned off your link roundup Section, that’s a signal to kill it or rework the format.

    The export also shows Section opt-in dates, so you can track adoption over time. If a new Section isn’t attracting opt-ins after 30 days, it’s probably not differentiated enough from your main feed.

    One edge case: Substack doesn’t let you set default Section subscriptions for new subscribers. Everyone who signs up is automatically opted into all Sections. You can’t onboard new readers into just your free Section and gate the premium one—they get everything unless they manually toggle it off.

    This means your welcome email needs to explain what each Section is and how to manage preferences, or you’ll see higher unsubscribe rates from people who didn’t expect the volume.

    Section limitations

    Sections don’t have separate branding. The header, logo, and colour scheme apply to your entire publication. If you want each Section to feel visually distinct, you’re limited to post-level formatting.

    You also can’t schedule posts to different Sections at the same time. Substack’s scheduler works at the publication level, so if you want to send your main newsletter and a bonus Section on the same day, you’ll need to stagger the send times manually.

    Paid subscriptions apply across all Sections—you can’t charge separately for individual Sections. If you want to monetise one Section independently, you’d need a second Substack publication.

    Sections work best when your content streams share a core audience but vary in format, frequency, or depth. If you’re running a single newsletter and considering expansion, Sections are worth testing—but only if you’re prepared to let subscribers self-select out of the extra volume.

    Using Substack or considering it? Subscribe to One Two Three Send for more breakdowns of newsletter platform features that actually matter.

  • AI prompt version control: when edits break what used to work

    AI prompt version control: when edits break what used to work

    AI prompt version control: when edits break what used to work
    Photo by Alexander Sutton on Unsplash

    You’ve spent an hour tuning a prompt that finally generates clean product descriptions. Two weeks later, you tweak one sentence to fix a minor issue—and the entire output degrades. You can’t remember what you changed. You don’t have the old version. You’re starting from scratch.

    This is the hidden tax of working with AI tools as a solo operator: prompt drift. Unlike code, prompts rarely live in version control. Unlike templates, they don’t auto-save revisions. You iterate in a text field, overwrite what worked, and lose the breadcrumb trail back to stable output.

    If you’re using Claude, ChatGPT, or any API-driven AI tool more than once a week, you need a lightweight system to track prompt versions before an accidental edit costs you an afternoon of re-testing.

    Why prompts break when you edit them

    AI models are sensitive to phrasing, order, and context window position. A prompt that works today can fail tomorrow if you:

    • Reorder instructions (models often weight earlier instructions more heavily)
    • Add examples that conflict with existing tone guidance
    • Change a keyword the model latched onto as a formatting anchor
    • Expand context and push key instructions past the model’s effective attention span

    The problem compounds when you’re using the same base prompt across multiple workflows—email subject lines, social captions, outline generation. Edit the shared prompt to fix one use case, and you might break three others without noticing until next week.

    A three-file version control system that takes 90 seconds

    You don’t need Git. You don’t need a database. You need three text files per prompt, stored locally or in a synced folder:

    1. prompt_live.txt — the current production version you’re actively using
    2. prompt_archive.txt — append-only log of past versions with datestamps
    3. prompt_notes.txt — what you changed and why, in plain English

    Every time you edit a prompt that’s working, copy the old version into the archive file with today’s date before you overwrite it. In the notes file, jot down what you’re trying to fix. If the new version fails, you have a rollback path and context for why you deviated.

    This isn’t theoretical. I’ve rolled back four prompts this month after “improvements” tanked output quality. Each rollback took 30 seconds because I had the prior version timestamped and ready to paste.

    When to snapshot a prompt

    Not every edit needs archiving. Snapshot when:

    • The prompt generates output you’d publish without heavy editing
    • You’re about to change structure (adding/removing sections, reordering steps)
    • You’re testing a new model or API endpoint with the same prompt
    • You’ve spent more than 20 minutes tuning it—your time investment is the signal

    If you’re still experimenting and nothing works yet, don’t bother. Once a prompt crosses into “production” territory—meaning you rely on it weekly—start tracking.

    API users: commit prompts to your repo

    If you’re calling Claude or OpenAI via API and storing prompts as variables in scripts, treat them like code. Commit prompt changes separately from logic changes. Write a one-line commit message explaining the edit.

    I’ve seen operators bury prompt tweaks inside feature branches, then lose track of which version shipped. A prompt is configuration, not implementation—version it accordingly.

    For non-coders: a .txt file in Dropbox with date headers works just as well. The tool doesn’t matter. The habit does.

    What this prevents

    Version control won’t make your prompts better. It will stop you from making them worse by accident. It gives you:

    • A rollback option when new phrasing degrades output
    • A diff view (even manual) to spot what changed between working and broken states
    • Confidence to experiment, knowing you can revert in seconds
    • A reference library when you need to adapt an old prompt to a new workflow

    The overnight cost is near zero. Three text files. A two-second copy-paste before you edit. A one-sentence note about intent.

    The upside is measured in hours you don’t spend reconstructing a prompt that worked last month, before you “improved” it into the ground.

    Want more practical systems for solo operators running AI-assisted workflows? Subscribe to One Two Three Send for weekly breakdowns of what actually works—and what quietly breaks.

    Heads up — some links in this article are affiliate links. If you sign up through them, we may earn a small commission at no extra cost to you. We only recommend tools we use ourselves.

  • Stop using spreadsheets as product roadmaps—they hide dependencies

    Stop using spreadsheets as product roadmaps—they hide dependencies

    Stop using spreadsheets as product roadmaps—they hide dependencies
    Photo: Tamara Weißmann via Wikimedia Commons (CC BY-SA 4.0)

    Many solo operators run their product roadmap in a spreadsheet. Google Sheets, Airtable if they’re feeling fancy, sometimes just a Notion table. It makes sense at first—quick to set up, easy to sort by priority or launch date, and you already know how to use it.

    But spreadsheets fail at the one thing roadmaps need most: showing you what depends on what.

    When you shift a launch date or reprioritize a feature, a spreadsheet won’t tell you what else breaks. It won’t show you that the email automation you planned for August requires the API integration you just pushed to October. It won’t flag that your affiliate dashboard redesign depends on Stripe webhook changes you haven’t scoped yet.

    You find out when you sit down to build—and by then, you’ve already committed the time.

    Dependencies are invisible in rows and columns

    Spreadsheets organize information in one dimension: down. You can add columns for status, owner, priority, quarter. You can color-code cells. But you can’t see relationships between tasks without either memorizing them or writing them into a notes column that no one reads.

    Picture this: you delay a course platform integration by three weeks, move the date in the sheet and mark the row yellow. The email sequence you drafted, the landing page copy and the affiliate programme you just set up may all assume that integration is live, and nothing in the sheet tells you.

    None of that is visible in the spreadsheet. You have to scan every other row, check your notes and reverse-engineer what you have already forgotten.

    Visual roadmap tools surface what shifts when you move one piece

    Tools like Roadmunk, Craft.io, and ProductPlan aren’t just prettier spreadsheets. They let you draw dependency arrows: this feature blocks that one, this launch requires these three tasks to close first, this campaign can’t start until that integration is live.

    When you drag a card to a new date, the tool shows you—immediately—what else needs to move. Some tools auto-shift dependent tasks. Others flag conflicts and make you decide. Either way, you’re not flying blind.

    Dependency features and prices differ by tool and plan, so check each tool’s current pricing page before you commit. In a tool that tracks dependencies, moving a feature highlights everything that depends on it, including items you may have forgotten.

    When a spreadsheet still works

    If you’re managing fewer than ten active initiatives and nothing depends on anything else—pure parallel work—a spreadsheet is fine. If your “roadmap” is really just a prioritized backlog with no sequencing, keep the sheet.

    But the moment you have a chain—A before B, B unlocks C—you need a tool that can show you the chain. Otherwise you’re rebuilding the mental model every time you look at the list, and you will miss something.

    Migration is mostly a CSV import

    Many roadmap tools import from CSV, so check yours: export your spreadsheet and match the columns (title, description, status, date). The remaining work is drawing the dependency lines, which forces you to think through what depends on what.

    That’s not busywork. That’s the clarity you’ve been missing.

    Drawing the lines can surface circular dependencies: tasks that each assume the other will ship first. In a spreadsheet they just sit there, both marked “Q3”, both impossible.

    If you are still roadmapping in a spreadsheet, export it and try a visual tool on a free trial. Roadmunk, Craft.io, ProductPlan, even Trello with an automation to enforce dependencies: anything that shows you the graph, not just the list. Judge it by whether it shows you a dependency you had missed.

    What you can’t see, you can’t plan around. And a spreadsheet only shows you rows.

  • Course platform video bandwidth caps: what breaks at 1TB

    Course platform video bandwidth caps: what breaks at 1TB

    Course platform video bandwidth caps: what breaks at 1TB
    Photo by Growtika on Unsplash

    Solo operators launching video courses don’t usually budget for bandwidth. They budget for hosting, maybe email delivery, occasionally CDN fees. But video bandwidth caps on course platforms catch most creators off guard—and the bills or throttling that follow can kill a launch.

    If you’re hosting video on Teachable, Thinkific, Podia, Kajabi, or any other all-in-one course platform, you’re subject to bandwidth limits that aren’t always printed on the pricing page. Some platforms enforce soft caps with overage fees. Others throttle playback speed or pause delivery entirely until the next billing cycle. A few don’t enforce caps at all—until you reach a threshold that triggers a support email asking you to upgrade or migrate.

    Here’s what actually happens when you approach 1TB of monthly bandwidth, which platforms enforce what, and how to structure your video library to stay under the wire.

    What counts as bandwidth

    Bandwidth isn’t storage. Storage is how much disk space your video files occupy. Bandwidth is how much data gets transferred every time someone streams or downloads your content.

    If you upload a 500MB video and 100 students watch it in full, you’ve consumed 50GB of bandwidth that month. If 1,000 students watch it, that’s 500GB. Add multiple videos per course, partial rewatches, and mobile users who retry streams after dropping connections, and usage climbs faster than enrollment.

    Most platforms count both video streams and file downloads. Some count thumbnail previews and adaptive bitrate variants separately. A few platforms pre-encode video at multiple resolutions (360p, 720p, 1080p) and serve whichever the viewer’s connection requests—but every variant streamed counts against your cap.

    Platform-by-platform caps and overages

    Teachable doesn’t publish a bandwidth limit on any plan, but enforces a soft cap around 1TB per month. Go over and you’ll get a support email suggesting you upgrade to a custom Enterprise plan or move large files to external hosting (Vimeo, Wistia, YouTube unlisted). Overage fees aren’t automatic; you negotiate them case-by-case.

    Thinkific caps bandwidth at 2TB/month on the Pro plan ($199/month) and enforces hard throttling if you exceed it mid-cycle. The Growth plan ($399/month) raises the cap to 5TB. If you hit the limit, video playback slows to buffer every few seconds until the calendar month rolls over.

    Podia has no published bandwidth cap and claims unlimited delivery, but community threads report that accounts serving more than 3–4TB/month get flagged for review. Podia’s support typically asks you to compress videos or switch to external hosting rather than charging overages.

    Kajabi enforces a 1TB cap on the Basic plan ($149/month), 2TB on Growth ($199/month), and 5TB on Pro ($399/month). Overages cost $1 per additional gigabyte, billed automatically. A single viral week can add hundreds of dollars to your invoice if you’re near the cap.

    If you’re using WordPress with a membership plugin (MemberPress, Restrict Content Pro, Paid Memberships Pro) and hosting video files directly, your bandwidth is determined by your hosting plan. Shared hosting typically caps monthly transfers at 1TB; managed WordPress hosts like WP Engine and Kinsta allow 2–5TB depending on tier. Exceed it and you’ll either pay overage fees ($0.10–$0.50/GB) or get throttled until you upgrade.

    How to stay under 1TB without compressing quality to death

    The most effective fix is offloading video to a specialist platform and embedding it in your course. Vimeo Pro ($20/month) includes 1TB of bandwidth and charges $0.02/GB over that—far cheaper than most course platform overages. Wistia starts at $24/month for 250GB and scales to custom plans with negotiated bandwidth pools.

    Both platforms let you embed videos with domain-level privacy (only your course site can play them) and disable download buttons to prevent students from hoarding files locally. You lose native course-platform analytics, but both Vimeo and Wistia offer heatmaps, engagement graphs, and completion tracking you can export or pipe into Zapier.

    Another approach: compress videos before upload using Handbrake (free, open-source). A 1080p MP4 encoded at H.264 with a constant rate factor (CRF) of 23 looks nearly identical to CRF 18 but weighs 30–40% less. For talking-head courses with minimal motion, CRF 26 is often imperceptible and cuts file size in half.

    If your course platform supports adaptive bitrate streaming, upload only 720p and 1080p variants. Most students on mobile default to 720p, and forcing a 1080p-only stream wastes bandwidth without improving their experience.

    When to pay for bandwidth vs. when to change architecture

    If you’re spending more than $100/month on bandwidth overages or nearing your cap every cycle, it’s worth splitting video hosting from course delivery. Keep your course platform for enrollment, payment processing, and student dashboards—but serve video from Vimeo, Wistia, or a dedicated video CDN like Bunny Stream ($0.005/GB).

    Bunny Stream is the cheapest option for high-traffic courses. You upload once, and Bunny encodes and delivers video globally for half a cent per gigabyte. A course consuming 2TB/month costs $10 in Bunny bandwidth, compared to $200–$400 in platform overages or plan upgrades.

    The tradeoff: you lose one-click upload workflows and native progress tracking. You’ll need to embed Bunny’s iframe player manually and connect view events to your course platform via webhook or API. For operators comfortable with light custom code, the savings are worth it. For everyone else, Vimeo or Wistia’s embed-and-forget workflow is the better middle ground.

    If you’re launching a video course this year, calculate your expected bandwidth before you pick a platform. Multiply total video file size by estimated student count and average watch rate. Add 20% for retries and partial views. If the result approaches 1TB, either compress harder, plan for external hosting, or budget for overage fees from day one.

    Want more breakdowns like this? Subscribe to One Two Three Send—every week we cover the infrastructure, tools, and pricing details that solo operators actually run into.

  • Why solo operators need fewer analytics dashboards, not better ones

    Why solo operators need fewer analytics dashboards, not better ones

    Why solo operators need fewer analytics dashboards, not better ones
    Photo by path digital on Unsplash

    Open question: how many browser tabs do you have pinned right now for analytics tools?

    Google Analytics 4. Plausible or Fathom. Your newsletter platform’s dashboard. Stripe for revenue. Maybe a social media scheduler with engagement stats. Maybe a WordPress stats plugin.

    You’re swimming in data, but you still can’t answer basic questions without cross-referencing three platforms and doing the math in a spreadsheet.

    The problem isn’t that your analytics tools are bad. It’s that you’re using too many of them.

    The dashboard creep problem

    Every tool you add promises one thing it tracks better than the rest. Fathom gives you clean pageview data. GA4 tracks events and funnels. Your newsletter platform counts opens and clicks. Stripe tells you MRR.

    None of them talk to each other.

    So you end up with this: a reader clicks a link in your newsletter, lands on a blog post, scrolls to a paywall, and converts to a paid subscriber. That’s one journey. But you’re tracking it in four places:

    • Newsletter click-through in MailerLite or Beehiiv
    • Pageview and scroll depth in GA4 or Plausible
    • Conversion event in Stripe
    • Subscriber count in your membership plugin or Substack dashboard

    Want to know your true conversion rate from email to paid? You’re exporting CSVs and building pivot tables.

    Most solo operators don’t have time for that. So they stop looking. They check revenue once a week, glance at traffic when it spikes, and ignore everything else.

    Consolidation is a feature, not a compromise

    The knee-jerk response is to buy a better integration tool. Zapier, Make, or a dedicated analytics warehouse like Segment.

    That works if you’re running a team with a data analyst. For a solo operator, it’s just another dashboard to maintain.

    Here’s the alternative: pick one source of truth per question you actually need to answer, and delete the rest.

    If you run a paid newsletter, your newsletter platform’s dashboard is your primary metric. Revenue, open rates, churn—it’s all there. You don’t need GA4 tracking the same pageviews unless you’re also running a separate blog with ad revenue.

    If you run a content site with affiliate income, your traffic analytics tool (GA4, Plausible, Fathom) is the anchor. Stripe is secondary—you check it when payouts arrive, not daily.

    If you sell a course or productised service, Stripe or your payment processor is the single source of truth. Traffic is context, not the core metric.

    One dashboard per business model. Everything else is noise.

    What to delete right now

    Start with anything that duplicates a metric you already track elsewhere.

    If your newsletter platform shows subscriber count and growth rate, you don’t need a separate spreadsheet updating those numbers weekly. If Stripe shows MRR and churn, you don’t need a SaaS metrics dashboard calculating the same figures with a two-day delay.

    Delete any tool you haven’t logged into in the last 30 days. If you’re not checking it, you’re not using the data. And if you’re not using the data, the tool is just spending CPU cycles and cluttering your mental model.

    Delete any analytics layer that requires manual export to be useful. If you have to download a CSV, open Excel, and join two tables to answer a question, that tool isn’t serving you—it’s creating work.

    The one dashboard you actually need

    Here’s what works for most solo operators:

    One real-time traffic dashboard (Plausible, Fathom, or GA4 if you’re already fluent in it). One revenue dashboard (Stripe, or your newsletter platform if you’re subscription-first). One weekly export of the metric that matters most to your business model—usually traffic sources, conversion rate, or churn.

    That’s it. Three views, not six. You can check all three in under five minutes, and you’ll have enough context to make decisions without second-guessing the data.

    If you can’t make a decision with the data a tool provides, the tool isn’t solving a problem—it’s becoming one.

    Want more operator-to-operator breakdowns like this? Subscribe to One Two Three Send and get one article like this in your inbox every week—no fluff, no filler, just the infrastructure decisions that matter.

    Heads up — some links in this article are affiliate links. If you sign up through them, we may earn a small commission at no extra cost to you. We only recommend tools we use ourselves.

  • SEO keyword cannibalization: when two posts compete and both lose

    SEO keyword cannibalization: when two posts compete and both lose

    SEO keyword cannibalization: when two posts compete and both lose
    Photo by Merakist on Unsplash

    You publish a post on “best email automation tools” in March. It ranks on page two. In June, you write another piece on “email automation platforms for small businesses.” Google now has two targets for the same search intent—and neither breaks page one.

    This is keyword cannibalization: when your own content competes against itself. Search engines don’t know which page to prioritize, so they split the ranking signal between both. You lose traffic, authority, and conversion potential on a topic you should own.

    Most solo operators don’t notice until months later, when a high-effort post underperforms and the analytics show an older piece siphoning impressions.

    How to spot cannibalization in your content

    Open Google Search Console. Navigate to Performance, then filter by query. Pick a target keyword you care about—something with commercial intent or steady volume.

    Click into the query detail. Scroll to Pages. If you see two or more URLs listed with similar impression counts, you have cannibalization. Neither page is dominant; Google is rotating which one appears in results.

    The clearest signal: impression counts split 60/40 or closer. If one page holds 95% of impressions, you don’t have a problem—that’s hierarchical ranking. But when impressions distribute evenly, search engines see both as equally relevant and neither as authoritative.

    Check position data next. Cannibalizing pages often hover between position 8 and 20. They’re indexed, they’re relevant, but they’re not winning because the ranking signal is diluted.

    Which post to keep, which to redirect

    Decide based on three factors: backlinks, traffic history, and content depth.

    In Search Console, compare total clicks over the last 90 days. The page with higher cumulative clicks has momentum. If the margin is tight, check external backlinks using a tool like Ahrefs or your hosting analytics referrer log. The page with more inbound links carries more authority.

    If both posts have similar metrics, pick the one with better content structure—clearer headings, more examples, updated tooling references. That’s your canonical target.

    Once you’ve chosen, you have two options:

    • 301 redirect: Delete or unpublish the weaker post and redirect its URL to the stronger one. This consolidates link equity and tells Google the authoritative page.
    • Canonical tag: Keep both posts live but add a rel="canonical" tag on the weaker page pointing to the stronger one. Use this when the weaker post still serves a secondary audience or internal linking structure.

    Redirects are cleaner. Canonical tags are useful when you want to preserve a slightly different angle but still signal priority to search engines.

    Merge content instead of deleting

    Before you redirect, audit both posts for unique value. The weaker post might have a better example, a clearer how-to section, or updated pricing data.

    Copy those sections into the stronger post. Rewrite transitions so the merge feels intentional, not Frankenstein. Update the publish date if your CMS supports it, or add a note at the top: “Updated July 2026 with new tools and consolidated guidance.”

    This approach gives you one definitive resource instead of two mediocre ones. Readers get more value per click, and Google sees a single, comprehensive answer.

    After merging, set up the 301 redirect from the old URL to the updated post. Traffic will consolidate within two to four weeks as Google recrawls and reassigns ranking signals.

    Prevent future cannibalization with a content map

    Track your target keywords in a spreadsheet. Three columns: Keyword, Primary URL, Publish Date.

    Before drafting a new post, search your own site. Use site:yourdomain.com "keyword phrase" in Google. If a post already covers the topic, update that post instead of creating a new one.

    If you need a new angle—say, a beginner’s guide vs. an advanced deep-dive—make the keyword targeting explicit. Use different modifiers: “how to start” vs. “advanced strategies for.” Give Google clear semantic separation.

    Internal linking also matters. When you mention a keyword in passing, link to your primary post on that topic. This signals hierarchy and reinforces which page you want ranking.

    Cannibalization isn’t a catastrophe, but it’s invisible drag on growth. Fix it once, and your best content starts working harder.

    One Two Three Send covers SEO, hosting, and content tooling for solo operators. Subscribe for weekly breakdowns of what actually moves the needle.

  • WordPress CDN purge delays: when edge caches serve stale content

    WordPress CDN purge delays: when edge caches serve stale content

    WordPress CDN purge delays: when edge caches serve stale content
    Photo by WebFactory Ltd on Unsplash

    You publish a correction to a post, hit update, and reload the page. It looks fine. Two hours later, a reader emails you about the mistake you just fixed—because they’re still seeing the old version.

    CDN edge caches don’t purge instantly. Even when your WordPress hosting provider or CDN plugin claims “automatic purge on publish,” the signal has to propagate across dozens or hundreds of edge nodes. Some refresh in seconds. Others take minutes. A few stragglers can hold stale content for five, ten, even fifteen minutes.

    If you run a content site, newsletter archive, or any business where correctness matters more than speed, you need to understand how purge delays work—and when to bypass the CDN entirely.

    How CDN purge propagation actually works

    When you update a WordPress post, your caching plugin or hosting control panel sends a purge request to the CDN. That request tells the CDN to invalidate the cached copy of a specific URL.

    But CDNs don’t store one copy of your page. They store dozens, distributed across edge nodes in different cities and regions. The purge request has to reach every node that cached that URL. Some CDNs use a hub-and-spoke model, where a central controller tells edge nodes to purge. Others rely on eventual consistency, where nodes check in periodically and sync purge lists.

    The result: purge latency varies by CDN provider, geographic distribution, and how the edge node last synced. Cloudflare’s global purge typically completes in under 30 seconds. StackPath and BunnyCDN clock in around 30–60 seconds. Older or budget CDNs can take two to five minutes. Regional edge nodes in low-traffic zones sometimes lag another five minutes beyond that.

    If your reader hits an edge node in Tokyo thirty seconds after you purge in New York, they might still see the cached version. The purge request is in flight, but hasn’t landed yet.

    When purge delays break your workflow

    For most blog posts, a one-minute delay doesn’t matter. Readers won’t notice. But three scenarios make purge lag painful:

    • Breaking news or time-sensitive corrections. If you publish live updates, product launches, or breaking analysis, stale caches mean early readers see outdated information. You can’t control which edge node they hit.
    • Email newsletter links that drive immediate traffic. You send a newsletter linking to a new post. Subscribers in different regions click within seconds of each other. Some see the post. Others hit a 404 or an older draft, because the edge node nearest them hasn’t purged yet.
    • Affiliate links, pricing, or legal copy. If you update an affiliate disclosure, a product price, or terms of service, you need every reader to see the current version immediately. A five-minute cache lag exposes you to inconsistency or compliance risk.

    Purge delays also compound when you use multiple caching layers. If your host runs Varnish or Nginx caching in front of the origin, and you layer a CDN on top, you have two purge queues. A plugin might purge the CDN instantly but leave the host cache intact for another sixty seconds. The CDN pulls the stale copy from the host, re-caches it, and now you’re waiting for both layers to expire.

    What to do when instant purge matters

    If you need guaranteed fresh content, you have three levers:

    Bypass the CDN for critical URLs. Most CDNs let you set cache rules per path. If you have a /live subdirectory or a specific post slug that needs zero cache lag, exclude it from edge caching entirely. The page will load slightly slower, but every reader sees the current version.

    Use query string cache busting. Append a unique query parameter to the URL when you update the post—something like ?v=1721203487. The CDN treats it as a new URL and skips the cache. This works for newsletter links, social shares, or any scenario where you control the inbound link. It doesn’t help organic search traffic, because Google ignores query strings in most cases.

    Monitor purge lag and escalate if necessary. If you’re on a managed WordPress host like BigScoots, WP Engine, or Kinsta, check whether they expose purge status in the dashboard. Some hosts show per-node purge completion. If you see consistent lag on specific regions, open a support ticket. Misconfigured edge nodes or stale DNS can cause outlier delays that support can fix.

    For most solo operators, the simplest fix is setting shorter cache TTLs on high-stakes pages. If your default CDN cache is set to 24 hours, dial it back to 5 or 10 minutes for posts you update frequently. You lose some performance benefit, but you cut the worst-case purge window from hours to minutes.

    The non-obvious detail: browser cache adds another layer

    Even if your CDN purge completes in ten seconds, readers might still see stale content for minutes or hours—because their browser cached the page. CDN purges don’t touch client-side caches.

    If your WordPress caching plugin or CDN sets Cache-Control: max-age=86400 (24 hours) in HTTP headers, the reader’s browser won’t even ask the CDN for a fresh copy until that timer expires. You can purge the CDN a hundred times; it won’t matter.

    Check your caching plugin’s browser cache setting. For posts you update often, set browser cache TTL to 5 or 10 minutes, not hours. That way, readers who reload the page within a few minutes will fetch the updated version from the CDN, even if their initial pageview hit a stale edge node.

    One more thing: if you’re testing purge behavior yourself, open an incognito window or clear your browser cache before each reload. Otherwise you’re testing your local cache, not the CDN.

    Got a caching or hosting question? Reply to this email—we cover reader questions every Sunday.