Skip to content
article

july was my worst month in 2 years. -$2,180 MRR. here is all of it, including the part where i double charged 143 people

the numbers, the billing bug, the launch that died, and the 6 things i am changing

Jo Brennan@jo_ships

August 3, 2026 · 3 min read

i almost didn't post this one. which is exactly why i'm posting it.

the numbers

  • invoice PDF API: $16,910 (-$1,510). churn 6.8% 📉
  • shopify order tagging: $10,610 (-$570). churn 5.2%
  • uptime monitor: $2,240 (-$100)

total $29,760. june was $31,940. that's back below april.

part 1: my biggest customer left

a payroll startup paying $1,100/month moved their PDF generation in-house. nice email, no drama, "we're consolidating vendors". that's $1,100 of the $1,510 right there. i knew one customer was 6% of the API revenue. i told myself it was fine. it was fine until it wasn't.

part 2: i double charged 143 people

base plans are normal stripe subscriptions. overages (PDFs above your plan) i calculate myself and add as invoice items once a month. a nightly job does it.

on july 9 at 11pm (yes. i know.) i shipped a migration that, among other things, reset a last_billed_at column on 143 subscriptions. the overage job ran at 2am, saw 143 subscriptions that had "never been billed" for june, and billed them. again.

$6,412 in overage charges, twice. i found out at 7:40am from an email that started with "are you kidding me".

what i did

  1. turned off the job
  2. refunded all 143 within 3 hours
  3. emailed every single one myself. not a template. ok, half a template
  4. gave every affected account a free month

cost: stripe doesn't give back the processing fees on refunds, about $4,100 of free months, 11 of the 143 cancelled anyway, and 2 one-star reviews on the shopify app from people who weren't even affected but were angry on principle. fair enough honestly.

the fix (the part you can steal)

the job had no memory of what it had already done. it trusted a timestamp column that any migration could touch. now:

ALTER TABLE overage_charges
  ADD CONSTRAINT one_charge_per_period UNIQUE (subscription_id, period_start);

the job inserts the charge row first, and only calls stripe if the insert succeeded. a second run fails loudly on the constraint instead of quietly charging people. i also send stripe an idempotency key built from the subscription id and the period, but stripe only keeps idempotency keys for about 24 hours, so the database constraint is the real guard. the key just covers retries on the same night.

and one dumb alarm: before the job charges anyone, it adds up the total and compares it with last month. over 130%, it stops and texts me. it would have caught july.

part 3: the launch that died

status pages for the uptime monitor. 5 weeks of work. launched july 22, here, on product hunt and in my newsletter (4,100 people).

3 signups. one was my mom. one was a bot. the third cancelled the next day and said "i thought this was something else".

i killed it on july 31. the code lives in a branch called rip-status-pages. i'm not linking it.

6 things i'm changing

  1. no deploys after 6pm. not "try not to". no.
  2. billing code gets a dry run and a diff against last month, every single time
  3. no single customer above 3% of a product's revenue without a contract
  4. no new product until the existing ones stop leaking
  5. i ask 10 customers before i build anything. for status pages i asked 0
  6. i stop pretending i'm fine at 11pm. i'm not fine at 11pm. nobody is

that's july. biscuit doesn't know. biscuit thinks we're doing great.

Written by

Jo Brennan

solo founder. no VC, no cofounder, no employees. 3 tiny SaaS products, $38.2k MRR last month (I post the numbers every month, good and bad). ex agency PM. denver. dog: Biscuit 🐶

Get the best of #bootstrapped_and_broke weekly

One email each Monday with the most useful posts. No spam; unsubscribe in one click.

Comments

40

Sign in to join the conversation.

Sign in to comment
Chloe Barnes@chloebuilds#3 in Bootstrapped and Broke
posting the bad month is why we trust the good months. also "one was my mom" got me, my mum signed up for my first app twice because she forgot her password x
17 net votes
Jo Brennan@jo_ships#1 in Bootstrapped and Broke
moms are the most loyal and least retained customer segment
13 net votes
Kenji Tanabe@kenjitanabe#9 in Bootstrapped and Broke
Thank you for posting this. I'm four months into building my own thing after the layoff and this is exactly the part nobody shows. The concentration point especially. I have one customer. It is 100% of my revenue. 🙃
11 net votes
Jo Brennan@jo_ships#1 in Bootstrapped and Broke
kenji, one customer is a contract, not a business yet, and that's ok at month 4. DM me, let's look at your pricing page together this week
6 net votes
Kenji Tanabe@kenjitanabe#9 in Bootstrapped and Broke
I will take you up on that. Thank you.
10 net votes
Dana Kowalski@dana_ml#7 in Bootstrapped and Broke
This is the most useful post on the site this month. One question, and it is not a gotcha: was the migration written with AI assistance? It matters for the time-log conversation.
17 net votes
Jo Brennan@jo_ships#1 in Bootstrapped and Broke
...yes. claude wrote the migration, i reviewed it at 11pm half asleep and approved it. the AI didn't double charge 143 people. tired me did. but yes, it goes in the log
5 net votes
Dana Kowalski@dana_ml#7 in Bootstrapped and Broke
That is a fair split of blame and a more honest answer than most people would give. The review is where the human part lives, and 11pm is where reviews go to die.
16 net votes
Arjun Mehta@arjunmehta#6 in Bootstrapped and Broke
Unique constraint on (subscription_id, period_start) is the right fix. In payments we also keep a billing preview table: compute everything into staging, diff against the last run, a human approves if the delta is above some X. Boring, and it has saved us more than once.
17 net votes
Jo Brennan@jo_ships#1 in Bootstrapped and Broke
the preview table goes in this week. stealing shamelessly
16 net votes
Emeka Nwosu@emeka_golang3 rep in Bootstrapped and Broke
The 24h window on Stripe idempotency keys catches many people, good that you said it. Your DB constraint is the real guard, the key is just a seatbelt
6 net votes
Sam Whitaker@segfault_sam5 rep in Bootstrapped and Broke
a migration at 11pm. i too like to live dangerously but i usually just eat gas station sushi
9 net votes
Ankit Khurana@faang_or_bust#10 in Bootstrapped and Broke
this is why you get a real job with a real QA team lol. 143 double charges imagine
-16 net votes
Oluwatobi Adeyemi@tobi_freelance6 rep in Bootstrapped and Broke
My brother, real jobs double charge too. Ask any Nigerian bank 😂
11 net votes
Ankit Khurana@faang_or_bust#10 in Bootstrapped and Broke
at least my salary doesnt go down 2k in a month lol
9 net votes
Jo Brennan@jo_ships#1 in Bootstrapped and Broke
neither does mine! i don't have one 🙃
9 net votes
Kuya Migs@kuya_migs_codes1 rep in Bootstrapped and Broke
Guys please 🙏 good post, let's keep it about the post
9 net votes
Trần Phương Thảo@thao_qa1 rep in Bootstrapped and Broke
As QA: a billing migration with no dry run is the true horror story of this year 🐞 but the fix list is very good, I will show point 2 to my team
5 net votes
Walt Hendricks@walt_mainframe1 rep in Bootstrapped and Broke
In 1994 our bank posted the same batch twice on a Friday afternoon. Forty thousand accounts. Your 143 refunds and a personal email to each of them is how it should be done. Most banks would have sent a letter in six weeks.
7 net votes
Frank Ostrowski@frank_cpp982 rep in Bootstrapped and Broke
A tired human approving code written by a machine at 11pm. The machine did exactly what it was told. Regards.
16 net votes
Ayesha Siddiqui@laravel_ayesha3 rep in Bootstrapped and Broke
Refunded in 3 hours with a personal email. Most companies take 3 weeks and send a chatbot. This is why people stay with you Jo
10 net votes
Trey Robinson@techwithtrey2 rep in Bootstrapped and Broke
the status page graveyard is real y'all. I count at least 40 status page products and they're all the same green bars. good call killing it
7 net votes
Renee Whitfield@renee_em3 rep in Bootstrapped and Broke
The line about not being fine at 11pm is the most important line in this post, and it's the one people will skip. Take a real day off this week. What's one thing you'd let slide to make that happen?
10 net votes
Jo Brennan@jo_ships#1 in Bootstrapped and Broke
honestly? support email for a day. autoresponder on, biscuit and i go to the mountains. thank you renee
11 net votes
Camilo Restrepo@camilo_parce3 rep in Bootstrapped and Broke
Jo, the 2 one-star reviews on the shopify app, want me to draft replies with the fix timeline? I can do it tonight, pues
10 net votes
Jo Brennan@jo_ships#1 in Bootstrapped and Broke
@camilo_parce yes please. you're the best
9 net votes
Vivek Chauhan@vivek_backend2 rep in Bootstrapped and Broke
Q: why calculate overages yourself and not use Stripe's metered billing? Genuinely curious, we run our own too but for UPI reasons
16 net votes
Jo Brennan@jo_ships#1 in Bootstrapped and Broke
historical reasons. i built it before i trusted stripe's usage billing, and migrating 400 accounts' billing is scary, especially after july lol. it's on the list. it's been on the list for 2 years
7 net votes
Danny Cho@danny_cho_la1 rep in Bootstrapped and Broke
A cheap alarm that saved me at a previous job: compare today's total charges to the rolling 30 day median and page if it's over 1.5x. Dumb, cheap, works. Your 130% rule is the same idea.
9 net votes
Elin Sjöberg@elin_pm3 rep in Bootstrapped and Broke
Killing status pages after nine days of data is the right call. Most teams I know would have kept it alive for a year "to give it a chance". Happy to be proven wrong!
15 net votes
Alasdair Reid@onprem_or_die1 rep in Bootstrapped and Broke
shoulda run the billing job on a box under yer desk. it would have crashed before charging anyone
13 net votes
Jo Brennan@jo_ships#1 in Bootstrapped and Broke
honestly that would have been better
13 net votes
Nadia Benali@nullbyte_nadia2 rep in Bootstrapped and Broke
Small security note : if one tired person can run a billing migration in prod at 11pm, so can a stolen laptop. Billing changes should need two keys.
20 net votes
Jo Brennan@jo_ships#1 in Bootstrapped and Broke
solo founder. both keys are me. biscuit doesn't have thumbs
8 net votes
Nadia Benali@nullbyte_nadia2 rep in Bootstrapped and Broke
Then one of the keys is "tomorrow morning you". Voilà, two-person rule 🙂
12 net votes
Onyeka Obi@onyeka_hottakes5 rep in Bootstrapped and Broke
funny how when an american founder messes up its called build in public and everyone claps. when a naija founder messes up its called a scam
16 net votes
Achieng Owino@achieng_devrel#2 in Bootstrapped and Broke
There is a real point buried in there, Onyeka, about how African founders get judged. But Jo refunded 143 people in three hours and told everyone. That is not the example. Write the post, I will read it.
9 net votes
Onyeka Obi@onyeka_hottakes5 rep in Bootstrapped and Broke
fair. the refund in 3 hours is real respect. still writing that post
9 net votes
Mariana Costa@mari_costa1 rep in Bootstrapped and Broke
I have a doubt: the free month for the 143, did it bring them back or they cancel anyway after? In ecommerce sometimes the compensation make people more angry
16 net votes
Jo Brennan@jo_ships#1 in Bootstrapped and Broke
11 cancelled anyway, most within a week. the rest stayed and 4 replied to say thanks. 2 upgraded later, which i did not expect at all
10 net votes