Worth saying out loud once: sometimes the answer is that the free tier is the wrong size for your app and the cheapest available fix is to stop engineering for zero.
Do the instrumentation first, because a genuinely broken listener will follow you onto the paid plan and cost real money there. But if after that you are simply an app that legitimately needs more than 50K reads a day, weeks of your time to stay at $0 is not a good trade. Decide it deliberately rather than drifting into it.
Tag the source on the signup itself from now on, even if it is just a query param you store. Retrofitting attribution is miserable and you only need it the moment the numbers get confusing.
41% open is fine for a week-old opt-in list. The reply rate is the tell, and reply rate is mostly a function of whether the email looks like it came from a person and whether the ask is small enough to answer in one line while walking.
"What are you using today?" is a good question but it is still an essay prompt to a busy tradesperson. "Are you doing this in a paper diary or software?" gets answered because it can be answered with one word.
After the subselect fix, check the sort. A list endpoint that orders by created_at desc limit 20 wants (user_id, created_at desc) as one index, not two separate ones. That took my equivalent query from 41ms to 3ms because it stopped sorting 8,000 rows to show 20.
Keep the VIES validation result. Screenshot or store the response with the invoice. If anyone ever asks why you did not charge VAT, that record is your answer.
Schedule it, sleep normally, be at your desk for the first four hours you are awake, and treat every comment like a support ticket. That is the entire playbook.
Honest take: going down a bit is not the disaster it feels like at the time. People screenshot the error, it becomes part of the story, some of them come back. What actually costs you is not having an email capture on the page that stayed up. A static fallback with one input field beats a perfect app returning 502.
Also look at what this is costing you. On most hosts middleware invocations are billed separately from function invocations, and a chatty matcher on a dashboard with 40 links in the viewport is a genuinely surprising line item. I cut mine 60% with matcher changes alone and the app behaved identically.
Normalise the address before you store it, and specifically for consumer Gmail: dots in the local part are ignored by Gmail, so all those dotted variants are the same mailbox, and everything after a plus is a tag on the same mailbox. Strip both, store the normalised form in a separate column with a unique index, keep the original for sending.
That single change kills the lazy version of this attack, which is the version you have. Someone who then goes and registers sixty real mailboxes on different domains is a different and much rarer problem.
One warning: the dots rule is a consumer Gmail behaviour. On a Google Workspace domain, dots are significant and two addresses differing by a dot can be two different people. Apply the rule to gmail.com and googlemail.com only, never as a general email rule.
Keep it in the existing resolution thread rather than resubmitting blind. A fresh submission gets you a new reviewer with none of the context and puts you back in the queue.
Attach annotated screenshots. Actual red circles and arrows on the button. It feels patronising to make and it works, because the person on the other side has forty apps in the queue today and about ninety seconds for yours.
Watch token size if you go the claims route. A user in one org is a tiny token. We have a support account that belongs to 90 orgs; embedding all of them produced a JWT that pushed the cookie past a limit some proxies enforce, and the failure mode is a 431 that is very hard to trace. Put the current org in the token, not the full list.
Also split the list into two columns before deciding: cost of goods sold versus overhead. Hosting, storage, bandwidth and transactional email scale with customers and belong in the first column. Your design tool and scheduler do not. 21% of revenue sounds alarming until you notice that maybe 9% of it is actually per-customer cost, and that is the number that determines whether the business works at scale.
You can also bulk in the fridge for part of it. Two hours on the counter, then into the fridge for the rest, then shape cold. Not traditional but summer is summer and it makes the timing controllable again.
Check who you interviewed. In a lot of businesses the person suffering the problem and the person who can spend $29 are different people, and enthusiastic interviews with the first group tell you nothing about the second. If you were talking to operations staff about a tool their manager buys, everything they said could be true and still produce zero cards.
Did this for two years. The build is not the cost. The cost is that every support conversation now starts with 'what version are you on' and you can't see anything. Someone will run a five month old image, hit a bug you fixed in March, and you'll spend a Saturday proving that.
If you do it: single container, one config file, no database of your own if you can avoid it, and a version check that phones home so you at least know what's out there. And a written rule that you support the current release and one previous. Write that rule down before you sell anything.
Since you asked about structure and not comfort, one more: you now have a disclosure to make, and the customer who found it will ask whether anyone else saw their data. Make sure you can answer that from logs. Being able to say "three requests, all from your own account, here are the timestamps" is the difference between a rough week and losing the account.
Had this happen at a similar size. Two clones, one of them literally with my testimonial text on their page with the names changed. I did nothing except keep shipping and answering support fast. One folded in about four months, the other pivoted into a different niche.
What did move the needle was writing to my existing customers before they heard about it elsewhere, in a normal human way: new lookalike in the market, here is what is different, tell me if you get a weird sales email. Three of them replied that they had already had the email and ignored it. That is the actual moat: the customer knowing who you are.
Cheapest test available: write one genuinely good page answering one specific question in that niche, publish it, and wait three months. Then read the actual queries in your search console rather than the estimates in a tool.
Real queries from real people beat every estimate, they cost you a weekend, and the page keeps working whatever you decide about the product. Two of the phrases I now build content around were things no tool ever showed me and no human would have guessed.
The connection limit detail matters more than the on-call process. That failure class is entirely preventable and it is the most common way small apps fall over.
Put a connection pooler in front of the database, cap the pool below the server limit, set a sane statement timeout so one bad query cannot hold connections open indefinitely, and add a health endpoint that acquires and releases a connection so your monitor is actually testing the thing that broke.
Spending an evening on that buys you more sleep than any alerting configuration, because the incident does not happen.
Practical bit since you are taking money for something unbuilt: keep the amounts small, refund fast and without argument, and be explicit that it is a pre-order rather than a subscription that has started. Card networks care about the description on the statement. This is 20 minutes of care, not a legal project, but do not skip it.
The two-container ISR cache split is exactly the kind of thing I would have found out about from a user, thank you. Might just run one container and accept 20 seconds of downtime on deploy honestly.
Update: bulk cut to three hours 20 minutes, ended at about 60 percent rise, same everything else. Loaf came out 9cm tall with an open crumb. It was the clock.
I have been going by the four folds and the clock rather than the volume. So a slack, soupy dough at hour five is the symptom rather than a hydration problem.