Author: onetwothreeadmin

  • 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.

  • Stripe subscription proration logic: what customers actually get charged

    Stripe subscription proration logic: what customers actually get charged

    Stripe subscription proration logic: what customers actually get charged
    Photo by Julio Lopez on Unsplash

    If you sell subscriptions—courses, membership sites, premium newsletters—you’ve probably set up Stripe and assumed the billing “just works.” It does, mostly. But the moment a customer upgrades mid-cycle, downgrades two weeks before renewal, or switches plans on day 28 of a 30-day billing period, Stripe’s proration logic kicks in. And unless you’ve tested it yourself, the charge your customer sees may surprise both of you.

    How Stripe calculates proration by default

    Stripe prorates subscription changes based on unused time. When a customer upgrades from a $10/month plan to a $50/month plan halfway through their billing cycle, Stripe:

    • Calculates the unused value on the old plan ($5 remaining)
    • Credits that amount toward the new plan
    • Charges the difference immediately ($50 – $5 = $45)
    • Resets the billing cycle to start today

    That last point matters. The customer’s renewal date shifts. If they subscribed on the 1st and upgraded on the 15th, their next charge is now on the 15th of every month—not the 1st.

    For downgrades, Stripe credits the unused portion and applies it to the next invoice. The customer pays nothing immediately, but their next bill is reduced. The billing cycle does not reset unless you configure it to.

    When proration breaks customer expectations

    Most confusion happens when customers upgrade near the end of their billing period. Imagine someone on a $20/month plan upgrades to $100/month on day 28 of 30. Stripe credits roughly $1.33 of unused time and charges $98.67 immediately. Two days later, the customer gets charged the full $100 again.

    From Stripe’s perspective, this is correct: the customer upgraded, got credited for two unused days, then hit their new monthly renewal. From the customer’s perspective, they just paid ~$199 in 48 hours.

    You can prevent this by disabling proration on upgrades and always charging the full amount immediately, or by using billing cycle anchoring to keep everyone on the same renewal date. The latter is common for SaaS products with tiered plans; the former works better for one-person operations where simplicity beats precision.

    Proration settings you can control

    Stripe gives you three levers:

    • Proration behavior: create_prorations (default), none, or always_invoice
    • Billing cycle anchor: Set a fixed day (e.g., 1st of the month) so all subscribers renew together, regardless of when they joined
    • Proration date: Override when Stripe calculates the proration from (useful if you’re backdating a plan change)

    If you’re running a paid newsletter and using a tool like Memberful or Substack, these settings are abstracted—you won’t see them. But if you’re building on Stripe directly (via API or a WordPress membership plugin), you control all three.

    For most solo operators, the simplest setup is:

    • Prorate upgrades (charge immediately, reset cycle)
    • Don’t prorate downgrades (apply credit at next renewal, keep cycle intact)
    • Skip billing cycle anchoring unless you have a strong ops reason (like batched fulfillment)

    One non-obvious tip: test with $0.50 test subscriptions

    Stripe’s test mode is helpful, but it doesn’t show you what the email receipt looks like or how your payment page renders the proration line item. Before you go live, create a live-mode product priced at $0.50/month and another at $2/month. Subscribe yourself, wait a few days, then upgrade. You’ll see:

    • Exactly what Stripe emails your customer
    • How the invoice PDF formats proration credits
    • Whether your customer portal (if you’ve enabled one) explains the charge clearly

    This costs you a few dollars in Stripe fees, but it’s worth it. The default invoice description—”Unused time on [plan name] after [date]”—makes sense to you. It may not make sense to someone who just saw $47 leave their account.

    If you’re using Stripe for subscriptions and haven’t touched proration settings, open your dashboard and click into a subscription product. Scroll to “Proration” under advanced settings. If it says “Automatic,” you’re using Stripe’s defaults. That’s fine for most cases—but now you know what happens when a customer clicks “upgrade” on day 29.

    Running a subscription business? Reply and tell us which billing edge case surprised you most. We’ll cover it in a future piece.

  • Traffic doesn’t compound—audiences do

    Traffic doesn’t compound—audiences do

    Traffic doesn't compound—audiences do
    Photo: Rose Abrams via Wikimedia Commons (CC BY 4.0)

    Every traffic guide tells you to chase SEO, write more posts, optimize meta descriptions, and wait for the compounding effect. The promise is simple: publish consistently, and traffic grows exponentially as old posts keep ranking.

    Except it doesn’t work that way for most solo operators.

    Traffic doesn’t compound. It decays. Google re-ranks your posts. Platforms change algorithms. Referral sources dry up. A post that drove 500 visitors last month might send 50 this month. You’re not building a snowball—you’re running on a treadmill.

    What actually compounds is audience: the list of people who opted in, the followers who see your posts directly, the group that comes back because they chose to. That’s the asset. Traffic is just a variable.

    The difference between traffic and audience

    Traffic measures eyeballs per page. Audience measures people who return. Traffic comes from discovery—search, social, referrals. Audience comes from capture—email, RSS, follows, bookmarks.

    Traffic requires you to re-earn attention every single time. Audience gives you a direct line. When you publish, your audience sees it. When you have an offer, they hear about it first. Traffic might spike and vanish. Audience sticks around.

    Here’s the math that matters: if you get 10,000 visitors this month and convert 2% to your email list, you add 200 people to an owned channel. Next month, you can reach those 200 people again—plus whatever new signups you get. That’s compounding. The 10,000 visitors? Most of them never come back.

    Why solo operators default to traffic

    Traffic is easier to measure. Google Analytics shows the chart going up. Social dashboards count impressions. It feels like progress.

    Audience-building is slower and harder to instrument. Email lists grow in double digits per week, not thousands. Social followers unfollow. RSS readers are invisible. There’s no dopamine hit from watching a subscriber count tick up by twelve.

    But traffic without conversion is just noise. You’re renting attention from Google, Meta, Reddit, or whoever sent the click. The moment they change the algorithm—or your post drops in rankings—it’s gone.

    Audience is owned distribution. It’s the only channel where you control both the message and the delivery.

    How to shift from traffic-first to audience-first

    This doesn’t mean stop doing SEO or writing for discovery. It means treating every inbound visitor as a potential long-term relationship, not just a session.

    Start with conversion rate, not traffic volume. If 5,000 visitors convert at 1%, that’s 50 new subscribers. If 2,000 visitors convert at 4%, that’s 80. The smaller number wins. Optimize your signup forms, exit-intent prompts, and content upgrades before you write another SEO post.

    Publish where your audience lives, not just where traffic might come from. If your email list is 2,000 people and your blog gets 8,000 monthly uniques, your list is still more valuable. They open, click, and buy. Random traffic just bounces.

    Measure retention, not sessions. Track how many subscribers are still opening six months later. How many social followers actually engage. How many RSS readers click through. If those numbers are low, your audience isn’t real—you just have a big list of dead contacts.

    The long game

    A 5,000-person email list that opens at 40% will outperform a blog with 50,000 monthly visitors and a 1% conversion rate. The list reaches 2,000 people on demand. The blog might convert 500 into one-time actions.

    Traffic gets you discovered. Audience gets you remembered. If you’re a solo operator building something that lasts longer than this quarter’s Google update, build the audience.

    One Two Three Send breaks down the tools and tactics that help you capture and keep attention—not just rent it. Subscribe and get one focused piece like this every day.

  • 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)

    Most solo operators I know 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.

    I learned this the hard way in March when I delayed a course platform integration by three weeks. I’d moved it in my roadmap spreadsheet, updated the target date, marked it yellow for “delayed.” What I didn’t notice until week two: the automated email sequence I’d already drafted referenced features from that integration. The landing page copy assumed it was live. The affiliate program I’d just onboarded partners for was built around it.

    None of that was visible in the spreadsheet. I had to manually scan every other row, check my notes, and reverse-engineer what I’d 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.

    I switched to Roadmunk in April. It’s $19/month for solo use, $49 if you want stakeholder sharing (I don’t). The first week felt like overkill—dragging cards, drawing arrows, setting up swimlanes for different product areas. But the second time I moved a feature, the tool lit up four other tasks that depended on it. Two I’d forgotten entirely.

    That paid for six months of subscription in one decision.

    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 takes an afternoon, not a week

    Most roadmap tools import from CSV. Export your spreadsheet, match the columns (title, description, status, date), and you’re 80% there. The remaining 20% is drawing the dependency lines—which forces you to actually think through what depends on what.

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

    I spent three hours migrating 22 initiatives from Sheets to Roadmunk. By hour two, I’d found three circular dependencies I didn’t know existed—tasks that each assumed the other would ship first. In a spreadsheet, they just sat there, both marked “Q3,” both impossible.

    If you’re still roadmapping in a spreadsheet, export it this week and try a visual tool for 30 days. Roadmunk, Craft, ProductPlan, even Trello with a Butler automation to enforce dependencies—anything that shows you the graph, not just the list. You’ll catch one missed dependency in the first week, and that’s worth the entire year of subscription.

    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.