Author: onetwothreeadmin

  • Beehiiv ad network earnings: what solo operators actually make per 1k opens

    Beehiiv ad network earnings: what solo operators actually make per 1k opens

    Beehiiv ad network earnings: what solo operators actually make per 1k opens
    Photo by Kanchanara on Unsplash

    The Beehiiv Ad Network promises simple monetisation: turn on ads, get paid per thousand opens, skip the sponsor outreach grind. But the earnings data operators share publicly rarely matches what you’ll see in your own dashboard—especially in the first few months.

    Here’s what the revenue actually looks like at different scale points, based on anonymised data from five operators running content newsletters between 2,500 and 45,000 subscribers.

    The CPM range: $8 to $38 per thousand opens

    Beehiiv‘s ad network pays on a CPM basis—cost per mille, or per thousand email opens. The range is wide, and it’s not just about list size.

    An operator with 6,200 subscribers in the personal finance niche reported an average CPM of $22 across Q2 2026, with individual sends ranging from $14 to $31. A tech commentary newsletter with 18,000 subscribers averaged $28. A general productivity newsletter with 12,500 subscribers sat at $11.

    The biggest CPM driver isn’t audience size—it’s advertiser category match. Finance, SaaS, and B2B newsletters pull higher rates because those advertisers pay more to reach decision-makers. Lifestyle, general productivity, and entertainment newsletters sit at the lower end, often below $15.

    Open rate matters too, but indirectly. Beehiiv’s ad network pays per actual open, not per subscriber. If your list has a 45% open rate, you’re monetising 4,500 opens on a 10,000-subscriber send. If it’s 25%, you’re monetising 2,500. The CPM stays the same; your total payout shrinks.

    Monthly revenue at different tiers

    At 5,000 subscribers with a 40% open rate and two sends per week, you’re looking at roughly 16,000 opens per month. At a $15 CPM, that’s $240. At $25, it’s $400.

    At 15,000 subscribers with the same cadence and open rate, you hit 48,000 opens monthly. That’s $720 at $15 CPM, $1,200 at $25.

    At 40,000 subscribers, assuming open rates drop slightly to 35% and you send twice weekly, you’re near 112,000 opens per month. At $20 CPM, that’s $2,240. At $30, it’s $3,360.

    These numbers assume consistent ad fill rate—Beehiiv doesn’t guarantee an ad in every send. Operators report fill rates between 70% and 95% depending on niche and time of year. In January and September, fill drops. In Q4, it peaks.

    When the math breaks down

    The ad network becomes less attractive once you can sell direct sponsorships. A single sponsor paying $500 for a dedicated slot in one send to 15,000 subscribers beats four weeks of ad network revenue at typical CPMs—and you control placement, messaging, and relationship.

    Operators also report that ad creative quality varies. Some ads are single-line text with a tracking link. Others are multi-paragraph placements that disrupt reading flow. You can reject individual ads, but doing so regularly lowers your fill rate and delays payout timing.

    Beehiiv’s payout threshold is $25, and payments process via Stripe 30 days after the calendar month closes. If you earn $18 in July, you won’t see it until you cross $25—possibly September or later if your list is small.

    Who should turn it on

    The ad network makes sense for operators between 3,000 and 20,000 subscribers who don’t want to manage sponsor relationships yet, or who publish in niches where direct sponsorship deals are hard to close. It’s passive income with near-zero effort once enabled.

    It also works as a revenue floor while you build a media kit and test sponsor outreach. You’re not leaving money on the table while you figure out pricing and positioning.

    But if you’re over 25,000 subscribers and getting inbound sponsor interest, or if you’re in a high-value niche like SaaS, finance, or developer tools, you’ll earn more by selling direct—even at lower volumes.

    Want more breakdowns like this? Reply and tell us what monetisation model you want numbers on next—affiliate rev share, course launch cohorts, or paid community tiers.

    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.

  • ConvertKit’s custom field limits: when segmentation hits a ceiling

    ConvertKit’s custom field limits: when segmentation hits a ceiling

    ConvertKit's custom field limits: when segmentation hits a ceiling
    Photo: Caladusp via Wikimedia Commons (CC BY-SA 4.0)

    ConvertKit lets you add up to 100 custom fields per account. That sounds generous until you’ve been running a newsletter for two years, tagging readers by interest, purchase history, geography, onboarding date, referral source, content preferences, and engagement tier.

    Then you hit the wall.

    The limit isn’t documented prominently. You discover it when the “Add Field” button grays out, or when an automation fails to create a new field and your sequence quietly stops working.

    What counts toward the limit

    Every custom field you create—whether it’s a text field, number, or date—counts toward the 100-field cap. This includes:

    • Active fields currently in use
    • Archived fields you’re no longer using but haven’t deleted
    • Fields created by integrations (Zapier, API calls, third-party forms)

    Deleting a field frees up a slot, but only if you’re certain no automation or segment references it. ConvertKit doesn’t warn you before deletion, and there’s no “undo.”

    Tags, by contrast, are unlimited. Subscribers are unlimited. It’s only custom fields—the structured data layer—that hits a ceiling.

    Where operators waste field slots

    Most operators I’ve audited are using 40–60 fields. The bloat comes from:

    Redundant date fields. Separate fields for “trial_start,” “trial_end,” “first_purchase,” “last_purchase,” “onboarding_completed.” You can often collapse these into tags with date-based automations, or store only the field you’ll actually query.

    One-off campaign tracking. A field for every lead magnet, webinar, or promo. If you’re not segmenting on it after 90 days, archive or delete it.

    Text fields that should be tags. A “interests” field with comma-separated values like “SEO, WordPress, email” is harder to segment than three tags. ConvertKit’s segment builder can combine tags with AND/OR logic; custom field text matching is clunkier.

    Legacy fields from old integrations. A Typeform you used once in 2024, a Zapier zap you turned off, a WordPress plugin you uninstalled. Each left fields behind.

    How to design segments that scale

    If you’re approaching the limit—or want to avoid it—here’s the structure that works:

    Use tags for categorical data. Interests, content preferences, lead sources, engagement tiers. Tags are unlimited, combinable, and easier to audit.

    Reserve custom fields for values you’ll calculate or compare. Numbers (purchase count, total spend, engagement score), dates (signup date, last click), or IDs (Stripe customer ID, external CRM reference).

    Audit every 90 days. Export your field list. Flag anything unused in the last quarter. Archive first, delete after another 30 days if no automations break.

    Document field purpose and owner. Keep a spreadsheet. Column A: field name. Column B: what it tracks. Column C: which automations or segments use it. Column D: date created. When you hit 80 fields, you’ll thank yourself.

    What happens if you hit the cap

    Automations that try to create or update a field beyond the 100th slot fail silently. The automation continues, but the field write doesn’t happen. You won’t get an error email. The subscriber moves to the next step as if nothing broke.

    If you’re relying on that field for downstream segmentation—say, tagging high-intent leads based on a quiz score—you’ll lose data without noticing until you spot the gap in your reports.

    ConvertKit support can’t raise the limit. It’s a hard platform cap, same across all pricing tiers.

    The workaround: delete unused fields, or rethink your data model. Some operators move complex segmentation logic into an external CRM (like Brevo or a dedicated CDP) and sync only the essential fields back to ConvertKit. That adds complexity, but it scales past 100.

    When to stay under 50

    If you’re running a solo operation with fewer than 10,000 subscribers, aim to stay under 50 fields. It forces clarity. Every field you add should answer: “What decision does this let me make that I can’t make with tags?”

    If the answer is “nothing,” use a tag.

    Most operators don’t need purchase history in a custom field—they need a “purchased” tag and a “last_purchase_date” field for recency-based re-engagement. That’s two slots instead of five.

    ConvertKit’s segmentation is powerful, but it rewards restraint. The ceiling exists whether you plan for it or not.

    Hit a segmentation problem you can’t solve with tags? Reply and tell us what you’re trying to track—we’ll feature operator solutions in a future issue.

    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.

  • Newsletter referral programs pay per signup—but measure net growth

    Newsletter referral programs pay per signup—but measure net growth

    Newsletter referral programs pay per signup—but measure net growth
    Photo by Defrino Maasy on Unsplash

    Most newsletter referral programs report one number: gross signups. Someone shares your link, three people subscribe, you see +3 in the dashboard. The referring subscriber unlocks a reward tier. Everyone’s happy.

    Except gross signups don’t tell you whether those three people are still reading 60 days later—or whether they bounced the moment the referrer claimed their prize.

    If you’re running a referral program through Beehiiv, SparkLoop, or a similar tool, you need to track net referral growth: how many referred subscribers remain engaged after the incentive window closes. Otherwise, you’re paying for churn with extra steps.

    Why gross referral counts mislead

    Referral programs reward the act of signing someone up, not the quality of that subscriber. If your reward tiers unlock at 3, 10, or 25 referrals, the person sharing your link is optimized for volume. They’ll post it in group chats, tag friends who aren’t interested, or share it in communities where your topic is tangential at best.

    Those signups count. The platform credits them. But six weeks later, half of them have unsubscribed or gone cold. Your list grew by 25, but your engaged audience grew by 12. You paid the referrer’s reward in full.

    This isn’t theoretical. One operator I spoke with ran a referral campaign offering a $50 Amazon gift card at 10 referrals. Average churn rate for referred subscribers in the first 90 days: 48%. For organic signups in the same period: 22%. The campaign grew the list by 340 subscribers. Six months later, 140 of them were still active. Cost per retained subscriber: higher than a modest Facebook ad budget would have delivered.

    What to measure instead

    Track referral cohorts separately from organic signups, and measure engagement at 30, 60, and 90 days post-signup. Most ESPs let you tag subscribers by source; if yours doesn’t, add a hidden field or custom property when someone arrives via a referral link.

    Compare:

    • Open rate at day 30: Are referred subscribers opening at the same rate as organic signups?
    • Unsubscribe rate by day 60: When does referred churn plateau?
    • Click rate on monetized content: If you’re running sponsorships or affiliate links, do referred subscribers engage with revenue-driving content?

    If referred subscribers churn or disengage faster than organic, your referral program is subsidizing vanity metrics. A list of 10,000 with 40% engagement beats 15,000 with 25% engagement in every scenario that matters: deliverability, sponsor value, product conversion.

    When referral programs still make sense

    Referral mechanics work when:

    • Your content has strong word-of-mouth fit—people genuinely want to share it, and the reward is a bonus, not the primary driver.
    • You’re willing to adjust reward tiers based on retention data, not just signup volume.
    • You can afford to treat referred subscribers as a separate, lower-intent cohort and nurture them differently in your first 90 days.

    If you’re below 1,000 subscribers and still defining your audience, a referral program will accelerate list growth but may also dilute signal. You’ll spend months figuring out what content works for two different cohorts instead of one.

    Above 5,000 subscribers, referral programs become more defensible—but only if you’re already retaining >70% of organic signups past 90 days. If your baseline retention is weak, a referral program will amplify the problem, not solve it.

    One non-obvious fix

    Delay reward fulfillment by 60 days. Instead of unlocking rewards the moment a referrer hits 10 signups, unlock them 60 days after the tenth signup—and only if at least 7 of those 10 are still subscribed.

    This shifts the incentive from volume to quality. Referrers will share your link with people more likely to stick around, because they only get paid if those subscribers stay. It also filters out referral farmers who game the system by cycling through throwaway emails.

    Most referral platforms don’t support conditional reward logic natively, but you can build it with a weekly script that checks subscriber status and manually triggers rewards. It’s friction, but it’s worth it if you’re spending four figures a year on referral incentives.

    If you’re running a referral program right now: pull your referral cohort data for the last 90 days and compare retention to organic signups. If referred churn is more than 10 percentage points higher, either tighten your reward criteria or redirect that budget to a channel with better unit economics.

    Want more breakdowns like this? Subscribe to One Two Three Send—we cover the operational details most online-business newsletters skip.

    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.

  • AI content rewriter tools break your voice—here’s the tradeoff

    AI content rewriter tools break your voice—here’s the tradeoff

    AI content rewriter tools break your voice—here's the tradeoff
    Photo by Deepak Gupta on Unsplash

    Most AI content rewriter tools promise the same thing: take your draft, smooth out the rough patches, tighten the prose, and hand back something publishable in seconds. They work. The output is cleaner. But every operator who uses them regularly hits the same problem after a few weeks: the writing stops sounding like them.

    This isn’t about quality. The rewritten version is often technically better—shorter sentences, fewer filler words, clearer structure. The problem is that it’s better in a way that erases the small habits and choices that make your voice distinct. The casual aside. The sentence fragment. The specific word you’d use instead of the synonym the model picked.

    Readers don’t consciously notice voice most of the time, but they feel it when it shifts. If your last ten posts had a consistent rhythm and suddenly post eleven reads like it came from a different person, open rates drop. Replies dry up. The content still works, but the connection weakens.

    What rewriters actually change

    AI rewriter tools—whether standalone like Wordtune or built into larger platforms like Jasper and Copy.ai—operate by rephrasing your input to match patterns the model learned from training data. That data skews toward polished, formal, widely-published writing. The model doesn’t know your tics. It doesn’t know you always use “folks” instead of “people” or that you start half your paragraphs with a dependent clause.

    Here’s what typically gets flattened:

    • Sentence rhythm. If you write in a mix of long and short bursts, the rewriter will smooth it into medium-length sentences.
    • Colloquialisms. Casual phrases get swapped for neutral equivalents. “A pain to set up” becomes “difficult to configure.”
    • Redundancy you use for emphasis. Repeating a word or idea for effect gets trimmed as inefficiency.
    • Personality markers. Em dashes, parenthetical asides, rhetorical questions—anything that breaks formal structure tends to get rewritten or removed.

    None of this is wrong. But if those elements are why your readers recognize your writing, stripping them out is a problem.

    When rewriters make sense

    There are situations where flattening your voice is the correct tradeoff. If you’re writing help documentation, product updates, or onboarding emails, clarity beats personality. Readers aren’t there for your voice—they’re there to solve a problem or understand a feature. A rewriter can take a tangled explanation and make it scannable in seconds.

    Rewriters also help when you’re stuck. If you’ve written the same paragraph three times and it still feels off, running it through a tool can break the loop. You won’t keep the output verbatim, but it gives you a new angle to edit from.

    And if English isn’t your first language, rewriters handle grammar edge cases faster than you can look them up. The risk is still there—your voice might get smoothed out—but the time saved often outweighs it.

    How to use them without losing yourself

    If you’re going to use a rewriter regularly, treat it like a first-pass editor, not a replacement for your judgment. Here’s the workflow that works:

    Write the full draft first. Don’t rewrite as you go. Get your ideas out in your natural voice, then decide which sections need help.

    Rewrite in chunks, not whole pieces. Run one paragraph or section at a time. If you feed an entire post into a rewriter, you lose control over which changes matter and which don’t.

    Edit the rewrite. Don’t publish the output as-is. Read it aloud. If a sentence doesn’t sound like something you’d say, change it back or meet halfway. The goal is to use the tool’s structure while keeping your word choices.

    Keep a voice reference. Save three or four posts you’re proud of—ones where the voice feels right. Before you hit publish on something that’s been rewritten, compare it. If the tone feels off, you’ll catch it.

    The long-term cost

    The risk isn’t just that one post sounds different. It’s that if you rely on a rewriter for every piece, you stop practicing the skill of editing your own voice. Over six months, your drafts start to sound more like the tool’s output even before you run them through it. You’re training yourself to write in a way that needs less rewriting, which means writing in a way that sounds like everyone else using the same model.

    This is fixable, but it requires noticing it’s happening. If you’ve published twenty posts in the last two months and none of them feel like you anymore, the rewriter is doing too much of the work.

    Voice is one of the few competitive advantages solo operators have. It’s free, it’s hard to replicate, and it’s why readers pick your site over the fifty others covering the same topics. Rewriters are useful tools, but they’re not neutral. Every time you use one, you’re making a tradeoff. Just make sure you’re choosing it, not defaulting to it.

    Trying to balance speed and voice in your own workflow? Reply with what you’re struggling with—I’ll cover reader questions in an upcoming piece.

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