nandi/frqpublic Fork
⑂ f4ea36a
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

A page nobody is reading costs nothing

`min_containers=1` held one container up for all 720 hours in a month.
Modal bills container uptime rather than requests, so a warm container
waiting on a page nobody had open cost exactly what one serving it did --
and that idle keep-warm, not traffic, was the whole of this app's usage.

Nothing here needs to be warm. It is a static bundle and a relay, with no
state in memory that a restart would lose, so the only thing the pin bought
was the cold start it avoided -- a registry pull of the small second stage
and a python, which is what the two-stage Dockerfile made cheap in the
first place.

`scaledown_window=300` keeps that from being every visitor's problem: five
idle minutes before shutdown, so a reading session pays the cold start once
and never again while they are here. `/api/v1/og` alone -- one upstream
fetch per link, cached nowhere -- keeps the container busy for as long as
anybody is actually reading.

Verified against the deployment rather than the docs: one container while
requests came, and zero five and a half minutes after the last one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-20T18:51:34-07:00 Browse files
f4ea36a parent: 391922b
modified .modal/web/app.py +16 -1
@@ -60,10 +60,25 @@ PORT = 8000
6060 SERVE = f"python3 /srv/webserve.py {PORT} /srv/web"
6161
6262
63-@app.function(cpu=1, memory=1024, timeout=3600, min_containers=1)
63+@app.function(cpu=1, memory=1024, timeout=3600, scaledown_window=300)
6464 # One container answering many requests: a static bundle costs nothing per
6565 # request, so scaling out on concurrency would buy cold starts and nothing
6666 # else.
67+#
68+# No `min_containers`, which is the difference between paying for a month and
69+# paying for the hours anyone is here. Modal bills container uptime rather
70+# than requests, so a warm container held for a page nobody is reading costs
71+# the same as one serving it -- and this is a static bundle and a relay, with
72+# nothing in memory that a restart would lose. The cost of that is a cold
73+# start on the first hit after the window below: a registry pull of the small
74+# second stage and a python, seconds rather than a compile, which is what the
75+# two-stage Dockerfile bought.
76+#
77+# `scaledown_window` is what keeps that from being every visitor's problem.
78+# Five minutes of idle before shutdown means a session pays the cold start
79+# once and a reader clicking between channels never does -- and `/api/v1/og`
80+# alone, one upstream fetch per link with no caching, keeps the container
81+# busy for as long as anybody is actually reading.
6782 @modal.concurrent(max_inputs=100)
6883 # `web_server` waits for the port to accept a connection and then proxies to
6984 # it, so the command has to keep running — `Popen` and return, not `run`.
@@ -60,10 +60,25 @@ PORT = 8000
60 SERVE = f"python3 /srv/webserve.py {PORT} /srv/web"60 SERVE = f"python3 /srv/webserve.py {PORT} /srv/web"
61 61
62 62
63-@app.function(cpu=1, memory=1024, timeout=3600, min_containers=1)63+@app.function(cpu=1, memory=1024, timeout=3600, scaledown_window=300)
64 # One container answering many requests: a static bundle costs nothing per64 # One container answering many requests: a static bundle costs nothing per
65 # request, so scaling out on concurrency would buy cold starts and nothing65 # request, so scaling out on concurrency would buy cold starts and nothing
66 # else.66 # else.
67+#
68+# No `min_containers`, which is the difference between paying for a month and
69+# paying for the hours anyone is here. Modal bills container uptime rather
70+# than requests, so a warm container held for a page nobody is reading costs
71+# the same as one serving it -- and this is a static bundle and a relay, with
72+# nothing in memory that a restart would lose. The cost of that is a cold
73+# start on the first hit after the window below: a registry pull of the small
74+# second stage and a python, seconds rather than a compile, which is what the
75+# two-stage Dockerfile bought.
76+#
77+# `scaledown_window` is what keeps that from being every visitor's problem.
78+# Five minutes of idle before shutdown means a session pays the cold start
79+# once and a reader clicking between channels never does -- and `/api/v1/og`
80+# alone, one upstream fetch per link with no caching, keeps the container
81+# busy for as long as anybody is actually reading.
67 @modal.concurrent(max_inputs=100)82 @modal.concurrent(max_inputs=100)
68 # `web_server` waits for the port to accept a connection and then proxies to83 # `web_server` waits for the port to accept a connection and then proxies to
69 # it, so the command has to keep running — `Popen` and return, not `run`.84 # it, so the command has to keep running — `Popen` and return, not `run`.