My version of this was hard pod anti-affinity with more replicas than nodes, which is the same arithmetic trap as yours. Six nodes, eight replicas, requiredDuringScheduling. Two pods Pending permanently and a very confused me. Preferred rather than required is right for almost everything that is not a database.
Marek Vrba
@migration_marek
Full-stack dev on a two-person team. I migrated our app from Prisma to Drizzle over one very long weekend and I am happy to tell you which parts were worth it and which were not.
42 credit Contributor
- From answers
- 0
- From questions
- 42
If you're going to hit this on every model, a client extension does it once:
prisma.$extends({ result: { invoice: { total: { needs: { total: true }, compute: (i) => i.total.toString() } } } })
Slightly magic, but better than remembering in 30 call sites.
connection_limit caps the pool inside one process. You have roughly 48 warm instances, so you got 48 pools of one, which is exactly what you measured. There is no client-side setting that can cap a number your platform decides.
Use the pooled endpoint: on Neon that is the host with -pooler in it, which is pgbouncer in transaction mode. Keep connection_limit=1 as well, and add pgbouncer=true to the URL so the client stops using named prepared statements.
What you give up in transaction mode: session state. SET that outlives a statement, advisory locks held across statements, LISTEN/NOTIFY, and long interactive transactions all behave differently or not at all. For CRUD over HTTP requests, none of that matters.
One book, one change, ninety days before you buy another. Most of the stack is the problem.
It was being generated inside the deploy script with no tty, which I inherited and never questioned. Moved generation to local, added the grep. Thursday looks a lot less exciting now.
Three tests that will catch most of them without opening a book. First, acronyms: if the claimed origin is an acronym and the word is older than roughly the twentieth century, it is essentially always false, because acronyms as a way of making words are a modern habit and pre modern people did not think that way about initials. Second, the single named originator: real word histories are usually gradual, dialectal and messy, so a story that pins a word on one identifiable person at one identifiable dinner party is doing narrative work rather than historical work. Third, the too tidy explanation of an oddity: if the story exists mainly to explain why a word is spelled strangely or why it means two things, be suspicious, because the story was probably reverse engineered from the oddity.
For the diffing part: store a content hash and the previous object key on each row. Then a diff is two R2 gets and no scanning, and the common case: nothing changed: is answered from D1 alone without fetching anything.
Agreed as a general rule, and it doesn't rescue this case: his rows are over the hard limit. But yes, the separate-table trick is the right hedge for the small-blob version.
The single-store version isn't a tradeoff, it's not available. D1's maximum size for a string, blob or table row is 2,000,000 bytes: your 3mb snapshots literally cannot be inserted.
And the ones that do fit will end the experiment on storage anyway: a database on the paid plan tops out at 10gb and that ceiling can't be raised. 40k snapshots a month at even 1.5mb average is 60gb a month. You'd hit the wall in about five days.
R2 for the bytes, D1 for url, timestamp, content hash and the object key. That's the design, and it's the design regardless of taste.
The thing I got wrong writing my own was not the password hashing, which is one library call. It was the reset flow.
My reset tokens weren't single use, didn't expire for 24 hours, and were logged in plaintext by my email provider's webhook. Somebody with access to an old inbox could have reset an account any time that day. Nobody exploited it, I found it myself, and it still cost me a weekend and a very awkward changelog entry.
If you keep passwords, write the reset flow last, carefully, and test that a used token is dead.
The line is "does anyone lose data if this is wrong". Prototype nobody depends on: push all day, it is faster and you will change the schema eleven times before lunch. Anything with a paying user or a support obligation: migrations, reviewed, applied by the deploy.
Self insuring the kit is defensible, but do not let anyone read that as skipping liability. The venue asking for a certificate is asking about the other policy entirely.
Indistinguishable on local devices. The only time anyone notices is right after I restart the hub, when everything shows as not responding for a minute and I get a text about it.
Yes, there's an HTTP API. POST to /accounts/{account_id}/d1/database/{database_id}/query with an API token in the Authorization header as a bearer token, send the sql and params in the body, and you get rows back as json.
That's a supported endpoint, not a workaround, and it's what wrangler is using under the hood when you run wrangler d1 execute --remote. For a nightly script producing a csv it's exactly right: no worker in the path, no shared secret you invented yourself, and the credential is revocable from the dashboard.
And put an expiry on it. Reporting scripts outlive the people who wrote them and a token with no end date is how you end up with a credential nobody can account for.
Numbers for the mechanics. Drop the TTL on the records you are going to change to 300 seconds at least a day before the move, and importantly do it before the old TTL has expired, which means planning further ahead than feels necessary. Make the change, verify, then put the TTLs back up. My eight hour outage was caused by a 24 hour TTL on records I changed at 9am, so the fix propagated at the speed of yesterday's decisions.
Disagreeing on the stand, specifically because you are in a flat. A wall-mounted folding stand takes up nothing when folded, holds the bike at a height where you are not kneeling on a hallway floor, and is the difference between doing a job now and putting it off until the weekend. In a house with a garage I would agree with buying it last. In a hallway, the thing that removes friction is what gets used.
We went the postgres row-level security route for exactly this, setting the tenant on the connection per request. It genuinely does mean a forgotten check can't leak, because the database refuses.
Honest costs: debugging is worse, because a policy silently returning zero rows looks identical to a missing record. Connection pooling needs care so a pooled connection never carries someone else's setting. Migrations get more awkward. I'd do it again for a product handling other people's customer data, and I would not bother for an internal tool.
Easy sharpening is not proof of anything either way, plenty of excellent carbon steel sharpens fast. It only tells you something when combined with how it holds up.
The tiering was the piece I did not know. That plus our DaemonSet footprint accounts for essentially all of it.
I did the cheap shared host version to keep a quote competitive and it cost me a weekend. Outdated plugin, injected spam pages, host suspended the account, client's email went down with the site because it was all in one place, and I did the cleanup for free because I had chosen the host. The hosting saving over two years was less than the value of that weekend.
Counterpoint: keep the box. You have a system with two years of evidence behind it, and the phone cost you named is real and is not measured in any comparison anyone will show you. If you want the top end benefit without the phone, add a sixth and seventh box on longer cycles and you have captured most of it. Migrating a working habit to chase an efficiency gain is how people end up not studying at all for a month.
Do both. bigserial (or identity) as the internal primary key that all your foreign keys use, plus a public_id with a unique index for URLs and the API. Joins stay narrow, external ids stay opaque, and you can change the external format later without touching your relationships.
Prefix the public id by type, inv_01hq..., org_01hq.... It costs nothing and the first time you are staring at a log line with a bare id and no idea what table it is from, you will understand.