App runtime limits
Every limit the app runtime enforces, with its value, the source line and the test that fails if it breaks.
Each row gives the value, the file that sets it and the test that fails when the limit stops holding. A row marked unproven has a value in the code but no test or limits-lab probe that names it yet. Values are read from the code at the commit that added this page; the source column is the place to check them.
The worker README explains the two deadline layers: a compute loop is stopped by epoch interruption at the wall-clock deadline, and a stall inside a host call is bounded by that capability's own deadline.
Run limits
| Limit | Value | Source | Proof |
|---|---|---|---|
| Execution deadline | 1 to 300 seconds | apps/worker/internal/contract/contract.go (MinTimeoutSeconds, MaxTimeoutSeconds) |
contract_test.go cases "deadline zero" and "deadline too long"; TestDeadlineInterruptsComputeLoop |
| Compute fuel (backstop behind the deadline) | FuelPerSecond per second of deadline |
apps/worker/internal/wasmrt/runtime.go |
TestFuelBoundsCompute, TestFuelRateMatchesConstant |
| Memory per run | 128, 256 or 512 MB profile, chosen per app | apps/api/src/artifacthost/services/apps.py (PROFILES); contract.go (MemoryProfiles) |
limits-lab probe "allocate 512 MiB" is refused at the default profile |
| Concurrent runs per worker | 32 by default, FERMIUM_WORKER_CONCURRENCY |
apps/worker/internal/server/server.go |
TestBudgetNeverExceedsItsTotalUnderConcurrency covers the memory budget; the slot count itself is unproven |
| Memory admission budget | the container's cgroup limit less headroom, 2 GiB when none is set | apps/worker/internal/server/server.go |
TestBudgetNeverExceedsItsTotalUnderConcurrency |
| Queued runs per app | 8 | apps/api/src/artifacthost/services/apps.py (MAX_QUEUE) |
unproven |
Size limits
| Limit | Value | Source | Proof |
|---|---|---|---|
| App source | 1,000,000 bytes | apps.py (MAX_SOURCE); contract.go (MaxSourceBytes) |
TestSourceSizeIsBounded |
| Files per app | 64 | contract.go (MaxFiles) |
TestSourceSizeIsBounded |
| Run input | 65,536 bytes | contract.go (MaxInputBytes) |
TestInputSizeIsBounded |
| Run result | 1 MiB | apps/worker/internal/wasmrt/wasi.go (DefaultLimits) |
unproven by name |
| Guest log kept by the worker | 256 KiB | wasi.go (DefaultLimits) |
unproven by name |
| Run log stored by the API | 32,768 bytes | apps/api/src/artifacthost/services/app_output.py (MAX_LOG_BYTES) |
test_apps.py and test_app_log_regressions.py assert the stored log stays within it |
| Capability calls per run | 64 | wasi.go (DefaultLimits) |
unproven by name |
| Bytes per capability call | 1 MiB | wasi.go (DefaultLimits) |
unproven by name |
| Data operations staged per run | 200, and 524,288 bytes in all | apps.py (MAX_OPERATIONS, MAX_OPERATIONS_BYTES) |
test_app_data_and_hooks.py asserts the 200 limit; the byte limit is unproven |
| Webhook delivery body | 49,152 bytes | apps.py (MAX_HOOK_BODY) |
test_app_data_and_hooks.py |
| Webhooks per app | 8 | apps.py (MAX_HOOKS) |
test_app_data_and_hooks.py |
Network
| Limit | Value | Source | Proof |
|---|---|---|---|
| Outbound hosts | only those in manifest.network.allow |
apps/worker/internal/hostcap/hostcap.go |
limits-lab probes "reach a host not on the allowlist" and "reach localhost" |
| Private, loopback and metadata addresses | refused at connect time | hostcap.go |
TestForbiddenAddress; limits-lab probes "reach cloud metadata" and "reach a cluster-internal address" |
| Outbound fetch timeout | 10 seconds by default | server.go, cmd/worker/main.go (FERMIUM_WORKER_FETCH_TIMEOUT_SECONDS) |
TestNewRequiresFetchDeadline proves a deadline must exist; the 10 s value is unproven |
| Outbound response body | 5 MiB | hostcap.go (MaxBodyBytes) |
unproven |
| Redirects | re-checked per hop | hostcap.go |
unproven by name |
| Live table read timeout | 10 seconds by default | server.go (DataTimeout) |
unproven |
Sandbox
| Limit | Value | Source | Proof |
|---|---|---|---|
| Packages, Node built-ins, environment, sockets | refused | apps/worker/internal/wasmrt/wasi.go imports |
TestLimitsLabHoldsEveryGuardrail, probes "require a third-party package", "require a Node built-in", "read environment variables", "open a socket" |
| Writes to a table the manifest does not declare | refused | API data capability | limits-lab probe "append to an undeclared table" |
| One app's globals are invisible to the next | isolated per run | one Store per invocation |
TestConcurrentExecutionsAreIsolated |
Compute allowance
Included App compute per UTC month is 1 hour on Free, 5 on Starter, 10 on Team, 50 on Business and 200 on Enterprise. The source is INCLUDED_COMPUTE_HOURS in apps/api/src/artifacthost/app_models.py. Each app also has its own monthly spend ceiling, DEFAULT_APP_SPEND_LIMIT, which a workspace administrator can raise. See Plans and limits.
Reading the proof column
TestLimitsLabHoldsEveryGuardrail runs the apps/worker/examples/limits-lab app and fails if a probe that should be refused is allowed. Run it with go test ./... -run TestLimitsLab in apps/worker. Production runs of limits-lab are tracked on the T-102 card, not on this page.