Limits¶
Two kinds: the ones we chose, and the ones rented from somebody else. The second kind has shaped real decisions in this project, so this is the page to read before deciding something, not after.
External figures checked on 2026-08-21. Free-tier terms change; the numbers below are a starting point, not a contract.
The ones we chose¶
| Limit | Value | Where |
|---|---|---|
| Finished vlog length | 120 s | default_max_total_seconds |
| Single take | 20 s | default_clip_max_seconds |
| Raw footage allowed | 1.5× the finished length | recording_allowance_factor |
| Premiere | 24 h | premiere_hours |
| Strikes before removal | 3 | strikes_before_removal |
| Cadence | 3, 5 or 7 days | Chosen per circle |
| Upload, whole vlog | 200 MB | max_upload_bytes |
| Upload, one raw take | 100 MB | max_clip_bytes |
The allowance being larger than the result is deliberate: recording more than fits is expected, and the edit trims the last take and says what did not make it.
Rate limits we impose¶
Per client address, in memory, in app/api/ratelimit.py.
| Route | Limit |
|---|---|
POST /auth/login |
10 per 5 min |
POST /auth/register |
5 per hour |
POST /circles/join |
20 per hour |
PUT/POST /turns/… |
60 per hour |
POST /auth/password-reset/request |
3 per hour |
POST /auth/password-reset/confirm |
10 per hour |
POST /auth/refresh |
60 per hour |
POST /auth/password |
10 per hour |
The reset limits are load-bearing rather than precautionary. A request sends mail to somebody else's inbox, which is the one thing an abuser can do here without an account; and ten confirm attempts an hour against 31⁸ combinations is what makes an eight-character code safe enough to type in.
In memory is the important word: the counters live in the process, which is one of the two reasons the app must run as exactly one machine. Two machines would each count roughly half the requests and every limit would silently be double what it says.
The registration limit is easy to hit while testing — five accounts an hour goes quickly when a demo script makes two of them.
Rented limits¶
Neon — free plan¶
| Compute | 100 CU-hours per project per month (≈400 h at 0.25 CU) |
| Storage | 0.5 GB per project |
| Autosuspend | After 5 min idle, cannot be disabled |
| Point-in-time restore | 6 hours |
| Network transfer | 5 GB per month |
Two consequences worth doing the arithmetic for.
The scheduler interval is not a preference, it is a requirement. At 0.25 CU, staying awake all month costs about 182 CU-hours against an allowance of 100. The compute suspends after 5 idle minutes, so a tick every 60 seconds keeps it awake permanently and blows the budget. At 15 minutes it is up for roughly a third of the time — around 60 CU-hours — which fits, though not enormously comfortably. If anything else starts querying on a timer, redo this sum.
/health deliberately touches no database, or the 30-second health check would
undo the whole thing.
A 6-hour restore window is why scripts/backup.py exists. Neon's own
recovery covers a mistake noticed the same morning. It does not cover one
noticed on Monday, and it does not cover the account going away.
Cloudflare R2¶
- No egress charge. This is the reason R2 was chosen over S3: the shape of the product is one upload read by every member of a circle, forever.
- Served from
vlogroulette-media.mintos.space, a custom domain on the project's own zone, so Cloudflare's rate-limitedr2.devhostname is no longer in the path. - Storage is billed by volume and nothing in this app ever deletes a vlog, so it only grows. At 9 MB total today that is theoretical; it is a product decision waiting to be made, not a bug.
Fly.io¶
- One machine,
shared-cpu-1x, 1 GB. The memory is for FFmpeg on the fallback render, not for the API. - Not a limit but a constraint: the machine may not be scaled out. See decisions.md.
Expo — EAS, free plan¶
| Builds | 15 Android and 15 iOS per month |
| Concurrency | 1 |
| Build timeout | 45 min |
| Queue | Low priority; waits can exceed 90 min at busy times |
| EAS Update | 1,000 monthly active users |
This is the tightest limit in the project. August 2026 used 17 Android
builds, over the allowance, and a build cycle is nine minutes plus queueing.
It is why npx expo-doctor and the native compile job in CI matter: catching a
mistake locally is free, catching it on EAS costs one of fifteen.
Expo Push¶
Free and unmetered in practice, but it is a best-effort service in front of FCM
and APNs. It answers 200 to a message it is refusing — the verdict is in the
body. Sending is fire-and-forget everywhere except send_now, which exists so
scripts/push_check.py can tell you the truth.
Sentry — Developer (free) plan¶
| Errors | 5,000 per month |
| Retention | 30 days |
| Seats | 1 user |
| Replays | 50 per month (not used) |
Traces are switched off deliberately: this is about errors nobody sees, not latency nobody has measured. 5,000 errors a month is generous for a handful of users and would be consumed in minutes by a crash loop — if quota disappears suddenly, look for something failing repeatedly rather than assuming the plan is too small.
Firebase Cloud Messaging¶
No meaningful quota at this scale. The constraint is configuration, not volume:
google-services.json is baked into the binary at build time, so changing
Firebase project means a new build.