just use a model with a 1m window and stop worrying about it
Devon Aker
@r2_bucketeer
I moved 8TB of user uploads off S3 to R2 last year to stop paying for egress. Presigned URLs, multipart uploads and lifecycle rules are where I can usually help.
0 credit Newcomer
- From answers
- 0
- From questions
- 0
Bigger runners are real and cost roughly double per minute for double the cores. If the suite is on the critical path for every deploy that can be the cheapest fix you will ever buy. Do the workers change first though, otherwise you are paying more money for the same starvation with a longer wick.
Archiving a price stops new subscriptions being created against it. It does nothing to the ones that already exist - they hold a reference to that price and keep renewing until something explicitly cancels them.
Order I would do it in, today:
- page through
stripe subscriptions list --status active --limit 100and collect the ids subscription.cancel(id)on each, notcancel_at_period_end, since there is no product left to deliver- refund the last invoice on each one, in full
- then email, after the refunds are in flight, so the email says "already refunded" instead of "will refund"
The six disputes are the expensive part. You pay the dispute fee whether you win or lose, and a sudden cluster of them on a dormant account is how an account ends up under review. Refunding a charge that is already disputed does not withdraw the dispute either - you still have to respond to it.
Do not zip. The zip is a requirement you invented.
Generate a manifest per account: manifest.csv with path, size, sha256 and a presigned URL valid for 30 days, plus a six line script at the top of the page that loops over it with curl. For the handful of big accounts, issue a scoped read-only token and tell them to point rclone at their own prefix - a 14 GB customer has someone technical.
You get to keep the whole thing stateless, you never hold a byte in memory, and resumability is free because each object is its own request.
Test the policy from outside, as the app role. Set the role and the request claims exactly the way your platform does at runtime, then assert that selecting another tenant's rows returns zero rows.
Assert on the row count, not on an error message. Error text changes between versions and you will spend a morning on it for no benefit.
Sell it. $340 MRR with low churn and five years of history lists somewhere around 2.5-3.5x annual profit, so call it $6.5-9k before fees. There are people who run six of these at once and for whom your 4.5 hours is thirty minutes because they already have the runbooks.
You will not retire on it. You will also never think about the CSV import again.
Both, but mostly the deploy. Two separate problems that look like one.
The lock. Your job runs three times longer than lockDuration. The lock is renewed while the job is alive and reporting; a job that goes completely silent for 90 seconds looks dead to the stalled checker, which hands it to another worker. Either raise lockDuration comfortably above your p99 job time, or call job.updateProgress() every few seconds inside the work so the lock keeps being renewed. The second is better because it also gives you progress for free.
The deploy. Your old pods were killed with jobs in hand. Handle SIGTERM: stop accepting new jobs, await worker.close(), and set the container's termination grace period longer than your longest job. The default grace period is 30 seconds and your jobs are 90, so every deploy guarantees a pile of orphans.
Together those two explain the exact shape you saw - active climbing, completed flat, ids repeating.
Diligence connects to Stripe and reads it directly. Three subscriptions on one company domain paid by the same card is more suspicious than one honest big customer, and "I restructured the billing so it would look better" is the kind of discovery that ends a deal and follows you around. Fix the risk. Do not repaint it.
If you do own it, own the entitlement model rather than the receipts. One table saying who has access to what until when, written by whatever source happens to know - client, notification, reconcile job. Then the source can change without your app noticing.
"A DLQ is not an alert" should be on a poster. I have inherited two systems with tens of thousands of messages in a dead letter queue that nobody had ever opened, in one case including the entire first week of a paying customer's data.
You are not missing anything. The tutorials are describing a serial suite and nobody updated them.
The pattern that survives parallelism is: do not reset, isolate. Every test creates its own tenant/org/user with a unique id and only ever touches rows under it. Truncate once at the start of the run and never again.
Two things fall out of this for free:
- your app gets a genuine multi-tenancy test on every run, because a test that can see another test's data has found a real bug in your scoping
- adding workers costs nothing, because there is no shared mutable state to fight over
The version that scales further is a worker-scoped fixture: one seeded org per worker, created once, reused by every spec in that worker.
Use a maintained library rather than hand-rolling the table. The naive version is about forty lines and the missing four hundred are scheduling, retry with backoff, visibility timeouts, archiving completed rows, and the migrations to change any of that later without downtime.
Every hand-rolled queue I have inherited was correct for the first six months and then grew a WHERE status = 'stuck_weird' clause.