You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The title and the premise below were REFUTED - corrected 2026-08-05 (901e832, #155)
There is no idle tail. The container never slept at all. Node runs as PID 1 under the
Dockerfile's exec-form ENTRYPOINT, Linux gives PID 1 no default signal dispositions, and @cloudflare/containers stops an idle instance with SIGTERM and never escalates to
SIGKILL - so every stop request was silently discarded. The instance only ever went away
when a deploy replaced the placement.
The "~8.5 minutes" was a metrics backfill artifact.containersUsageAdaptiveGroups
backfills, and that figure came from reading the 5-minute buckets ~14 minutes after a test
render and treating the missing tail as sleep. Re-read once settled, the same window has every bucket present and the placement stayed allocated 29.3 hours straight - 352 of
352 buckets, no gaps - on a total of 5 worker requests. Wait at least an hour before
reading absence as sleep, and confirm with placementId continuity, never bucket presence.
Also refuted: the response-drain fix (2026-07-29) did not stop this on its own. Billing
shows 100% allocation every full day through 2026-08-03, including the five days after it
deployed. Keep the drain guard - the hazard is real - but do not credit it with the bill.
Neither fix is deployed as of 2026-08-05. See #168.
The original report follows unchanged.
#116 fixed the container staying awake forever. It now sleeps - but ~25x later than configured. Costs almost nothing at current traffic; worth understanding before traffic grows.
Measured
One POST /preview fired against production on 2026-08-03 (worker version 2e67470f, container app a039bbb1-c8a4-4ecb-b6f5-bb3a1209a093), with a fresh seed to miss the R2 cache:
fired 22:39:01Z
returned 22:39:11Z status=200 10.59s 350,848B image/png
Billing buckets from containersUsageAdaptiveGroups (a 1 GiB instance awake for a whole 5-minute bucket = 300 GiB-seconds):
bucket
GiB-seconds
22:35Z
35
22:40Z
455
22:45Z
170
22:50Z
absent - asleep
Awake roughly 22:39:00 -> 22:47:50, about 8.5 minutes, for a 10.6-second render. sleepAfter is "20s" in preview-service/worker/src/container.ts. Total billed: 660 GiB-seconds for one render.
Two things this same test confirmed, both good, both worth recording here because they were open questions:
basic (1 GiB) is genuinely live and sufficient. The disk-to-memory ratio reads 4.0 (1 GiB / 4 GB); every pre-deploy sample read 2.0 (standard-1, 4 GiB / 8 GB). A full 1024px Nauvis render completes inside it, cold start included.
The inflight-counter leak (isActivityExpired() returning false while inflightRequests > 0) makes a container sleep never, not late. A leaked counter has no timeout to expire. So this is a different mechanism.
The likely candidate is scheduling granularity in @cloudflare/containers' own alarm loop rather than anything in this repo. In dist/lib/container.js the loop computes minTime = Math.min(minTimeFromSchedules, minTime, this.sleepAfterMs) where minTime is already clamped to a coarse interval (the comment there reads Math.min(3m or maxTime, sleepTimeout)), then re-arms via setAlarm. If the effective floor is minutes, a 20s sleepAfter cannot be honoured no matter what it is set to. Unverified - it needs someone to actually trace the alarm scheduling rather than infer it from a comment.
Cost, so nobody over-reacts
At ~7 renders/day: 660 GiB-s x 7 x 30 / 3600 = 38.5 GiB-hours/month against 25 included in Workers Paid, so about $0.12/month. Against the ~$28/month #116 fixed, this is noise.
It stops being noise if traffic grows: the tail is a fixed ~8.5 minutes of 1 GiB per wake, so cost scales with the number of cold wakes, not with render work. At ~50 renders/day it is ~275 GiB-hours/month, or roughly $2.25. Requests that arrive inside an existing awake window are free, so bursty traffic is cheap and sparse traffic is not.
What would settle it
Fire two renders ~30s apart and confirm the second does not extend the tail (proves the timer is not being renewed by something in this repo's code path).
Set sleepAfter to something large, e.g. "10m", and check whether the tail changes at all. If the tail stays ~8.5 minutes regardless, the setting is inert and the floor is upstream - that is the decisive experiment and it is cheap.
Only then consider filing upstream at cloudflare/containers, with these numbers.
Do not "fix" this by lowering sleepAfter further. If step 2 shows the value is inert, changing it is theatre.
Use a seed you have not used before, or R2 serves a cache hit and the container never wakes. Omitting the Origin header is deliberate and passes - the worker's check is if (origin && origin !== env.ALLOWED_ORIGIN).
Then read the 5-minute buckets from containersUsageAdaptiveGroups. Note that wrangler containers instances <ID> is not a reliable check here: it reported state: running with an 80-minute-old created timestamp during an hour when billing showed zero allocation. It describes the placement, not whether you are paying.
#116 fixed the container staying awake forever. It now sleeps - but ~25x later than configured. Costs almost nothing at current traffic; worth understanding before traffic grows.
Measured
One
POST /previewfired against production on 2026-08-03 (worker version2e67470f, container appa039bbb1-c8a4-4ecb-b6f5-bb3a1209a093), with a fresh seed to miss the R2 cache:Billing buckets from
containersUsageAdaptiveGroups(a 1 GiB instance awake for a whole 5-minute bucket = 300 GiB-seconds):Awake roughly 22:39:00 -> 22:47:50, about 8.5 minutes, for a 10.6-second render.
sleepAfteris"20s"inpreview-service/worker/src/container.ts. Total billed: 660 GiB-seconds for one render.Two things this same test confirmed, both good, both worth recording here because they were open questions:
basic(1 GiB) is genuinely live and sufficient. The disk-to-memory ratio reads 4.0 (1 GiB / 4 GB); every pre-deploy sample read 2.0 (standard-1, 4 GiB / 8 GB). A full 1024px Nauvis render completes inside it, cold start included.Why this is probably NOT the bug #116 fixed
The inflight-counter leak (
isActivityExpired()returning false whileinflightRequests > 0) makes a container sleep never, not late. A leaked counter has no timeout to expire. So this is a different mechanism.The likely candidate is scheduling granularity in
@cloudflare/containers' own alarm loop rather than anything in this repo. Indist/lib/container.jsthe loop computesminTime = Math.min(minTimeFromSchedules, minTime, this.sleepAfterMs)whereminTimeis already clamped to a coarse interval (the comment there readsMath.min(3m or maxTime, sleepTimeout)), then re-arms viasetAlarm. If the effective floor is minutes, a 20ssleepAftercannot be honoured no matter what it is set to. Unverified - it needs someone to actually trace the alarm scheduling rather than infer it from a comment.Cost, so nobody over-reacts
At ~7 renders/day: 660 GiB-s x 7 x 30 / 3600 = 38.5 GiB-hours/month against 25 included in Workers Paid, so about $0.12/month. Against the ~$28/month #116 fixed, this is noise.
It stops being noise if traffic grows: the tail is a fixed ~8.5 minutes of 1 GiB per wake, so cost scales with the number of cold wakes, not with render work. At ~50 renders/day it is ~275 GiB-hours/month, or roughly $2.25. Requests that arrive inside an existing awake window are free, so bursty traffic is cheap and sparse traffic is not.
What would settle it
sleepAfterto something large, e.g."10m", and check whether the tail changes at all. If the tail stays ~8.5 minutes regardless, the setting is inert and the floor is upstream - that is the decisive experiment and it is cheap.Do not "fix" this by lowering
sleepAfterfurther. If step 2 shows the value is inert, changing it is theatre.Reproducing the measurement
Use a seed you have not used before, or R2 serves a cache hit and the container never wakes. Omitting the
Originheader is deliberate and passes - the worker's check isif (origin && origin !== env.ALLOWED_ORIGIN).Then read the 5-minute buckets from
containersUsageAdaptiveGroups. Note thatwrangler containers instances <ID>is not a reliable check here: it reportedstate: runningwith an 80-minute-oldcreatedtimestamp during an hour when billing showed zero allocation. It describes the placement, not whether you are paying.🤖 Generated with Claude Code
https://claude.ai/code/session_018AB7J1qK6kSBnJJmgDqMst