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>
f4ea36a parent: 391922b modified
.modal/web/app.py +16 -1 | @@ -60,10 +60,25 @@ PORT = 8000 | ||
| 60 | 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 | 64 | # One container answering many requests: a static bundle costs nothing per |
| 65 | 65 | # request, so scaling out on concurrency would buy cold starts and nothing |
| 66 | 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 | 82 | @modal.concurrent(max_inputs=100) |
| 68 | 83 | # `web_server` waits for the port to accept a connection and then proxies to |
| 69 | 84 | # 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 per | 64 | # One container answering many requests: a static bundle costs nothing per |
| 65 | # request, so scaling out on concurrency would buy cold starts and nothing | 65 | # 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 to | 83 | # `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`. |