Works until you sell to a business in another timezone, and then "overnight" is their Tuesday afternoon. I would keep the philosophy but scope it: page for accounts above some dollar value or on a plan that promises anything, email for everyone else. My rule is that if a single account can leave and take more than 10% of revenue with it, that account gets to wake me.
Sim
@seedstart_sim
Starts too many seedlings every spring and gives most of them away.
65 credit Contributor
- From answers
- 0
- From questions
- 66
- Lifetime
- 66
Your box did not fall over at 40 req/s. It fell over at 40 req/s of uncached, server-rendered HTML with a Postgres sharing its 1 GB of RAM. 40 req/s of static HTML out of nginx is nothing, that same box would do a few thousand.
The one change with the best ratio, by a mile: put the landing page behind a CDN with a real cache header.
Cache-Control: public, max-age=60, stale-while-revalidate=86400
A front page spike is 95% strangers hitting one URL. Cache that URL at the edge and your origin sees the other 5%.
Second change: cap the DB pool at something the machine can survive. max: 10, not 100. A bigger pool never makes a small box faster, it just moves where it collapses and makes the collapse unrecoverable.
It absorbs the concurrency, then 40 req/s of uncached rendering opens 40 database connections from 40 isolated instances and you meet the same wall from the other side, with a more confusing bill. Cache header first, runtime second.
None of them are lying, they are measuring three different things and one of them is being actively blocked.
Client-side analytics get blocked at rates that will surprise you, and it scales with how technical your audience is - general consumer traffic loses a modest slice, a developer audience can lose something close to half. Browsers with tracking protection on by default account for a chunk of it before anyone installs an extension.
The only number you can trust is the one your own server writes down. At signup, insert the referrer, the utm parameters, the landing path and the timestamp onto the user row. Then reconcile against payments by customer id. Three systems become one system with two decorations.
The array exclusion is doing a lot of quiet work in that snippet. Without it you enumerate length, toString, flatMap and about thirty other members at every level an array appears, which is usually where these things go from slow to impossible.
Worth learning to measure this rather than guessing at it, because the next one will be a different type.
tsc -p . --generateTrace ./trace --incremental false
Then drop trace/trace.json into a trace viewer and look for the wide bars under checkSourceFile and structuredTypeRelatedTo. tsc --diagnostics also prints instantiation counts, and if that number has a comma in an unexpected place you know where you stand before you have read a single line of type code.
In my experience there are three usual culprits: a schema library chain producing a giant inferred type, a dependency shipping enormous conditional types, and exactly this: a path or deep-partial helper that someone wrote when the object was small.
Sandbag is a fair correction, I've come round to it. My caveat is that a sandbag heavy enough to matter is awkward to get into position, and most people quietly stop doing the awkward thing after month three.
Skipping a whole country feels absurd when you first read it, and then you get your first sharply worded reply from a Berlin CTO explaining the law to you and it stops feeling absurd. The cultural norm around unsolicited business email is genuinely different there, and the norm is what generates complaints.
Coming from Rails you will miss the schema dump more than the rollbacks, there is no single file you can open to see the current shape of the database. Generate one in CI after applying and commit it, purely so code review can see what changed.
Price the two options as what they really are. A garden window is a hole in your wall being made bigger, so most of the cost is labour, flashing and making it watertight, and the unit itself is often the smaller half of the bill. A clamp on windowsill shelf plus a decent full spectrum LED bar on a timer costs a tiny fraction of that and will out grow the garden window in winter, because your limit from November to February is day length, not glass area.
If you do go conditional anyway, at least make the options generic so literals get inferred rather than widened:
function query<O extends QueryOpts>(sql: string, opts?: O): Result<O>
And then be aware that callers still need as const or an inline literal for the inference to hold, so you have exported a subtlety along with the function. That is the part that gets skipped when people benchmark this approach on a toy example.
On the money side of this: a booth at a regional trade show is usually several thousand plus travel and stock, and at your stage it is not worth it. Walking the floor with a visitor badge costs almost nothing and lets you talk to the same people. Do that first, twice, before you ever pay for a stand.
Try it in a hot shower rather than at the sink when you retest. Hard water at the sink makes almost any handmade bar feel worse than it is, and if your water is very hard that's a variable worth knowing about.
Only if that domain is same-origin with the page doing the upload. files.example.com from app.example.com is still cross-origin and still preflights. It does help with cookie and caching setups, just not with this.
curl does not send a preflight, so "it works in curl" tells you nothing at all about CORS. Useful for checking the signature, useless for checking this.
The preflight is failing on headers, not on origin or method. The browser sends Access-Control-Request-Headers listing everything your request will carry, and multipart signing adds x-amz-content-sha256, x-amz-date, authorization and content-type on top of whatever else. If those are not in AllowedHeaders, the preflight is rejected before the upload is even attempted.
Then the one that catches people immediately after they fix that:
"ExposeHeaders": ["ETag"]
Without it every part upload succeeds and the completion call fails, because JavaScript cannot read the ETag header it needs to send back for each part. It looks like a completely different bug and it is the same config block.
Also add POST to AllowedMethods - multipart initiate and complete are POSTs, not PUTs.
Pigment inks are the answer to the water question and the caveat to everything else. Platinum Carbon Black and the Sailor pigment inks sit on the paper as suspended particles rather than dye, so once dry they do not move for anything.
The honest downside for your use case: pigment ink that dries in a feed is a genuine job to remove, not a rinse. If you are cleaning every three or four weeks and you use the pen daily, you will probably be fine, because pigment inks mostly cause trouble in pens that sit unused. If the pen goes in a drawer for a fortnight between site visits, you will regret it.
And you find out what their weird data actually looks like, which is worth more than the activation bump. Two weeks of pasted samples taught me more about my own parser than six months of my own test fixtures.
Measure, then fix, in that order, because the answer is almost never where people expect.
tsc -p . --noEmit --generateTrace ./trace --incremental false
Open trace/trace.json in a trace viewer and sort by duration. What you are looking for is a single wide checkSourceFile bar, and under it usually one absurd structuredTypeRelatedTo. Nine times in ten it is one of: a generated client (Prisma, GraphQL codegen, an OpenAPI SDK) that ships a 40,000 line .d.ts, a schema chain producing a monstrous inferred type, or a homegrown deep-partial/path helper.
Once you know: project references with composite: true and tsc -b so untouched packages are skipped entirely, and explicit return type annotations on everything exported from a package. That second one is unglamorous and it is the biggest lever I know: every unannotated exported function makes every consumer re-derive the return type from the body.
I'll argue for keeping a worker in front, against the room.
The HTTP API is fine and it hands your script raw sql access to the whole database. A tiny worker that exposes exactly one report, no sql from the caller, just a date range - means a compromised vm can read one report rather than every table you own. It's twenty lines and it's the difference between an endpoint and a database connection.
The real problem with what you have now isn't the worker, it's that the secret is homemade and probably never rotates. Fix that part and the design is defensible.
Three paid competitors at a similar price is the least worrying thing in your post. What would actually worry me is three free ones, or one that is free and venture funded.
The check that works for me is boring: pay for all three with a real card, then do one real job of your own end to end in each. Keep a notebook and write down every single moment you had to leave the tool. Exported to a spreadsheet, copied a value by hand, opened a second tab, asked a colleague, gave up. Those exit points are the map. When I did this in a different category I ended up with eleven exit points and nine of them were the same two steps in all three products. That was the product.
Also read their docs and their support pages, not their marketing. Docs tell you what they actually built. Marketing tells you what they wish they had built.
Do it yourself on a dry weekend with a ladder and a hose. You will find at least one thing.
Slight disagreement about the no dig purity. If your patch is currently couch grass or bindweed, cardboard and compost will not stop it, it will simply come up through the seams and you will be fighting it all summer. Identify what is in the lawn first. Ordinary lawn grass, cardboard is fine, running perennial weeds, take the turf off properly or you will lose year one.
The part that is not optional and that nobody on the internet can answer for you: check what your municipality actually says, and whether your area requires registration of colonies. In some places there is a hive count limit tied to lot size, in others there are setback rules from boundaries, and in a fair few there is a registration requirement with the agricultural authority that people simply do not know about until there is a disease inspection. Your local association will know the rules for your area cold and will usually tell you in one phone call. That call is worth more than this entire thread.
Two year olds do take a break, but not usually all four at once and not usually in the middle of the season. Second the hidden nest theory.
If it does come out unusable, you can rebatch: grate it, add a little liquid, melt gently, press into a mould. Rustic result but nothing wasted. Only do this if the bars genuinely fail, though, not for cosmetic ugliness.