Edge Function limits, one ceiling at a time
When a deploy of a Supabase Edge Function breaks, the report usually says “we hit the limit”. There are several ceilings, most of them have a different lever, and two common failures are not ceilings at all. This reference takes each one on its own, states the documented figure next to what the platform returned when the figure was crossed, and ends with the order of questions that tells them apart.
Every figure marked measured comes from one battery of the
supabase-lab harness,
experiments/edge-function-limits,
run on 2026-09-02 against a Micro project in ap-southeast-1 on a Pro
organization, created and destroyed within the hour, with Supabase CLI 2.116.0
and Docker present on the operator machine. The docs page the figures are
compared against was read the same day.1 Plan entitlements were
read from a Free, a Pro and a Team organization. Anything not marked measured
is cited to the docs and was not run.
TL;DR:
- Functions per project is a plan entitlement.
function.max_countreads 100 on Free, 1000 on Pro, 2000 on Team, matching the docs; the July 2024 change that removed usage-based billing had set them at 25, 500 and 1000, and the Pro and Team figures doubled in the docs on 2026-08-18. A cap of exactly 1000 identifies a Pro organization on the current table (it was Team before 2026-08-18); confirm which organization before treating it as a platform limit. An entitlement row can sit above or below its plan figure (reasoned, not measured: all three lab organizations carried the default), so a cap that matches no plan figure points at the row rather than at the plan. - Bundle size is set by where bundling happens, and both ceilings return
the same 413. The Management API and the CLI’s
--use-apibundle server-side and refused an 8 MB source with413 request entity too large; the Dashboard deploys through the same endpoint and was not driven separately. The CLI’s default with Docker bundles locally, landed 8 MB in 32 s, and refused 24 MB with the same 413 body. The size entitlement (function.size_limit_mb) is 20 on every plan. - Secrets are four limits. 100 user-defined secrets per project, 24,576
characters per value, 256-character names, and no
SUPABASE_prefix. All four bit at exactly the documented boundary. The value ceiling counts characters: 24,576 three-byte characters (73,728 bytes) were accepted. - Deploys can report success and not land, with nothing to see. 24 Management API deploys, run 8 at a time, all returned 201 and 10 of the 24 were present afterwards. 8 concurrent CLI processes all exited 0 and 9 of 24 functions were present. No 429 surfaced in either run. Verify with a GET per function after every deploy round; the exit code is not the state.
- Invocation-time ceilings: exhaustion is one code, and the rest were
measured. CPU past 2 s and memory past 256 MB both answer
546 WORKER_RESOURCE_LIMITwith identical bodies. An active stream is cut between 395 s and 400 s on a paid project; the two log limits truncate at exactly 10,000 characters and 100 events; the documented recursive cap of ~5000 calls per minute did not bite at about 110,600 nested calls per minute. - Two documented restrictions did not hold as written. Port 587 was reachable from the runtime on two runs (port 25 hangs), and a static file sent through the API deploy was accepted with 201 and then missing at runtime.
The order of questions
Section titled “The order of questions”The same order, as text:
- Read the error string. A limit names itself: a count, a size, one of the four secrets limits.
- If nothing is named, ask how many deploys were in flight. A 413 under parallelism needs a serial reproduction before it is a size limit; a 429 is the control-plane throttle, not a quota; a 409 is two deploys of the same slug overlapping.
- Then read
GET /v1/projects/{ref}/functions/{slug}for every function the run claimed to deploy. A 2xx or an exit code of 0 with the function absent is silent loss.
A limit rejects deterministically with a named error and reproduces on a retry of the same input. Deployments that break intermittently, or that report success and then turn out not to have happened, point somewhere else. The sections below take the named limits first, then the failures that only look like one.
Which ceiling is it
Section titled “Which ceiling is it”| What was reported | Which ceiling | Lever |
|---|---|---|
| a hard cap on creating functions, at 100 / 1000 / 2000 or some other figure | functions per project, plan-gated with a per-organization override | read function.max_count on the org: if the row equals the plan figure the lever is a plan change; if it does not (above or below) the lever is a change to that row, which no public API performs |
413 request entity too large on a serial deploy | bundle size for that deploy path | move the deploy to local CLI bundling (20 MB) or shrink the bundle |
| 413 only when several deploys run at once | possibly parallelism (a reported shape; neither parallel run in this battery produced a 413) | reproduce the same bundle serially before quoting a size |
You can only store 100 secrets per project at maximum. | secrets count | fewer secrets per project; one secret per tenant in a shared project is an architecture question |
Too big: expected string to have <=24576 characters | secret value size | split or externalise the value |
Too big: expected string to have <=256 characters | secret name length | rename |
Secret name must not start with the SUPABASE_ prefix. | reserved prefix | rename |
546 WORKER_RESOURCE_LIMIT at invocation | CPU or memory at run time | reduce per-request work; long jobs do not fit an Edge Function on any plan |
504 IDLE_TIMEOUT at invocation | request idle timeout | stream or respond earlier |
| exit 0 or 201, function 404s afterwards | not a limit | verify per function, retry the difference, lower concurrency |
| 409 on deploy | not a limit | two deploys of the same slug overlapped |
429 ThrottlerException: Too Many Requests on a deploy or list call | the Management API throttle, 120 requests per minute per user; not a quota | space the calls out, then verify what landed |
429 {"code":"RATE_LIMIT_EXCEEDED"} on an invocation | an invocation-time rate limiter with a 1 s retry hint; a different limiter from the control-plane throttle above, and not the documented recursive cap (0.02 per cent of calls at about 110,000 nested calls per minute) | retry after a second |
| log lines cut at 10,000 characters, or a burst of log events thinning out | the two log limits; the invocation itself succeeded | shorten lines, batch events; nothing to retry |
| a long response stops after several minutes while the logs show 200 | the worker wall clock, 400 s on paid plans and 150 s on Free; streaming does not exempt it | move long work out of the request path |
Functions per project
Section titled “Functions per project”The docs table and the entitlement agree, and the entitlement is the thing that actually gates.1
| Plan | Documented maximum | function.max_count read from the entitlements API |
|---|---|---|
| Free | 100 | 100 (measured) |
| Pro | 1000 | 1000 (measured) |
| Team | 2000 | 2000 (measured) |
| Enterprise | Unlimited | not read |
The figures have moved. The July 2024 changelog entry that removed usage-based billing for Edge Functions set the caps at 25 on Free, 500 on Pro and 1000 on Team;2 the docs changed to Pro 1000 and Team 2000 in a commit dated 2026-08-18,3 and the Limits page read 100, 1000 and 2000 on 2026-09-02. That dates the documentation, not the platform: when the default entitlement rows moved is not published, and this page does not date it. A report quoting 500 is most likely reading a note written before the docs changed; confirm on the entitlement row, since a row set before a raise keeps its value. A report quoting 1000 from before 2026-08-18 matched the Team figure the docs carried at the time.
The figure is an entitlement row, not a constant. GET /v1/organizations/{slug}/entitlements returns a flat list keyed by feature,
and function.max_count carries a value and an unlimited flag per
organization.4 A row that differs from the plan’s documented
figure is an override, and an override replaces the plan default rather than
lifting it: it can hold an organization above the documented figure or below
it, and a documented raise does not touch a row that was set before it. So a
cap that matches no plan figure (800, say) points at the row, and the fix is
on the row; a cap that matches the plan figure points at the plan. This is
reasoned from what an override is, not measured: the three lab organizations
all carried the plan default. The same list carries
function.size_limit_mb, which read 20 on all
three organizations - one half of the argument, made in the next section,
that size is not a plan lever.
Function size, by deploy path
Section titled “Function size, by deploy path”The docs give two figures and attach them to deploy paths rather than plans.1 The battery sent the same random, incompressible source down each path and proved every acceptance from the outside by invoking the function and reading back the payload length.
| Bundling path | Documented ceiling | 2 MB | 8 MB | 24 MB |
|---|---|---|---|---|
| Management API multipart deploy (also the Dashboard) | 5 MB | 201, landed, invoke returns 2,097,004 bytes (measured) | 413 request entity too large, nothing created (measured) | not sent |
CLI --use-api | 5 MB (same path by construction) | not sent | exit 1, unexpected deploy status 413: {"message":"request entity too large"} (measured) | not sent |
| CLI default with Docker (local bundling) | 20 MB | not sent | exit 0, landed, invoke returns 8,388,460 bytes, 32 s (measured) | exit 1, unexpected create function status 413: {"message":"request entity too large"} (measured) |
Three things follow from that table.
Anyone deploying programmatically through the Management API is on the 5 MB
path by construction, which is the usual shape for a platform that generates
functions per tenant. The Dashboard is the same path. The docs attach the two
figures to deploy paths and to no plan, and the function.size_limit_mb
entitlement read 20 on Free, Pro and Team alike.
The rejection is a 413 on both paths, with the same body. The published OpenAPI document declares 201, 401, 402, 403, 429 and 500 for the multipart deploy endpoint and no 413,4 so this is an undeclared response: a client generated from the schema has no typed branch for it. The CLI wraps it differently (“deploy status” on the API path, “create function status” on the local path) but a customer relaying the message will say “413, request entity too large” either way. So the status code does not identify which ceiling was crossed, and it does not distinguish a real size rejection from the 413 that reports attribute to parallel deploys, a shape this battery did not reproduce (neither parallel run below produced a 413). Reproduce the same bundle serially before quoting a size limit.
On CLI 2.116.0 the visible flag is --use-api, which forces server-side
bundling. The older --use-docker flag is still accepted but no longer listed
in --help. With Docker present the default bundled locally.
Secrets: four limits
Section titled “Secrets: four limits”The docs list four figures under one heading, and the Management API declares
all four in the request schema of POST /v1/projects/{ref}/secrets
(maxItems: 100, name maxLength: 256 with pattern ^(?!SUPABASE_).*, value
maxLength: 24576).14 Each was probed at N and N+1.
| Limit | Documented | Measured at the boundary | Error, verbatim |
|---|---|---|---|
| Secrets per project | 100 | 100 user secrets stored; the 101st refused with 400 | You can only store 100 secrets per project at maximum. |
| Value size | 48 KiB (24,576 characters) | 24,576 accepted, 24,577 refused with 400 | 0.value: Too big: expected string to have <=24576 characters |
| Name length | 256 characters | 256 accepted, 257 refused with 400 | 0.name: Too big: expected string to have <=256 characters |
| Reserved prefix | names must not begin SUPABASE_ | refused with 400, nothing stored | 0.name: Secret name must not start with the SUPABASE_ prefix. |
The value ceiling counts characters. A value of 24,576 three-byte characters (73,728 bytes, well past 48 KiB) was accepted, so the “48 KiB” in the docs is 24,576 at two bytes per character rather than a byte limit.
Once a project has had any function deployed, GET /v1/projects/{ref}/secrets
also lists seven platform-managed entries with the reserved prefix (the
project URL, the anon and service-role JWTs, the publishable-key and
secret-key lists, the database URL, the JWKS). They do not count toward the 100:
a run that counted the 7 platform rows toward its total stopped at 93 user
secrets when the list read 100, and the next user secret (the 94th user
secret, the 101st entry in the list) was accepted. Count user-defined secrets
when reasoning about the ceiling.
Ask which of the four was hit before answering. A project pressing against 100 secrets usually means one secret per tenant inside a shared project, and that is a placement question rather than a quota one - see where a tenant should live.
When it is not a limit
Section titled “When it is not a limit”Limits reject deterministically with a named error. The battery drove the two
shapes that get misreported as quota problems and recorded, for every deploy,
both what was reported and what GET /v1/projects/{ref}/functions/{slug} said
afterwards.
Parallel deploys report success and do not land
Section titled “Parallel deploys report success and do not land”| Shape | Reported | Present afterwards | 429 / 409 / 413 seen |
|---|---|---|---|
| 24 Management API deploys, 8 in flight | 24 of 24 returned 201 | 10 of 24 | none |
8 concurrent CLI processes, 3 functions each, --use-api | 8 of 8 exited 0 | 9 of 24 | none |
Both rows are measured. The loss is not a read-timing artefact: a listing taken several minutes later, during cleanup, found exactly the functions the per-slug reads had found. Both losses came from distinct slugs deployed concurrently, while the same-slug rounds below produced 409s, monotonic versions and a healthy function. In these runs that rules out slug collision; where the loss happens on the platform side is not visible to this battery.
A public issue on the CLI reports the same shape at larger scale: 138 of 237
functions absent after a run in which the CLI reported success for all 237
and exited 0, with unexpected list functions status 429: ThrottlerException: Too Many Requests surfacing on some invocations but far from all of the
missing ones.5 In the lab run the throttle did not surface at all,
so the throttle explains none of the loss here rather than part of it. The
Management API’s documented rate limit is 120 requests per minute, tracked
per user for personal access tokens, and every call for the rest of that
minute answers 429 once it is crossed.6
The operational rule is the one that issue arrived at: after every deploy round, list what is actually deployed and retry only the difference. An automated bootstrap that trusts the exit code ships part of an environment and reports it complete.
Two deploys of the same slug
Section titled “Two deploys of the same slug”| Shape | Statuses | Version sequence | Function afterwards |
|---|---|---|---|
| 3 rounds of 2 concurrent deploys of one slug | 201 x3, 409 x3 | 1 -> 2 -> 3 -> 4, no regression | invoke 200 |
| 5 rounds of 4 concurrent deploys of one slug (second project) | 201 x9, 409 x11 (version read once per round after all four settled) | 1 -> 2 -> 3 -> 4 -> 5 -> 6, one bump per round even where two of the four answered 201, no regression | invoke 200 after every round |
| delete issued 150 ms into a deploy of the same slug | deploy 201, delete 200 | - | present, ACTIVE, invoke 200; one trial |
| delete issued 0-400 ms into a deploy, 10 rounds, then a redeploy of the same slug (second project) | deploy 201 x10, delete 200 x10 | no regression | 8 present and healthy, 2 absent (deploy 201 then absent, attributed to the delete that was in flight); all 10 redeploys 201 and invoke 200; 0 present-but-404 |
All measured. A 409 is what a same-slug collision looks like on this platform, and it is a different failure from a 429; do not merge them in a report. Two or three of every four concurrent deploys of one slug answer 409, the version moved forward once per round, and the function kept answering, so the same-slug metadata corruption this battery was built to look for did not appear in eight rounds across two projects. The delete-during-deploy path was the other shape it was built to look for; eleven rounds produced either a healthy function or a cleanly absent one, and the slug redeployed cleanly after every round. Ten and five rounds repeat the first wave’s result; they do not prove absence, and a run at higher concurrency could still find a signature.
There is no published safe concurrency figure. Any number quoted for “how many deploys in parallel is safe” is an estimate.
Restrictions that are not size or count limits
Section titled “Restrictions that are not size or count limits”These sit below the limits tables on the same docs page.1 Two of them break deploys outright, and two did not hold as written.
| Documented restriction | Measured |
|---|---|
Node libraries that require multithreading are not supported (sharp, libvips) | npm:sharp bundled and deployed (201); invocation answered 500 with an empty body. It fails at run time, not at bundle time. |
| Static files cannot be deployed using the API flag; use Docker with the CLI | Through the API deploy with static_patterns: 201, and the file was missing at runtime (NotFound ... /source/static/hello.txt, function 500s). Through CLI local bundling with static_files in config.toml: exit 0 and the file read back. The API path fails in the worst shape, a success that is not one. |
| Outgoing connections to ports 25 and 587 are not allowed | Port 25: connect hung to an 8 s timeout. Port 587: TCP connect to a public SMTP host succeeded, on two runs. Port 465, undocumented, also connected. Port 443 control connected. SMTP submission itself was not attempted. |
Serving HTML is only supported on custom domains; otherwise a GET returning text/html is rewritten to text/plain | GET arrived as text/plain. A POST returning the same body kept text/html; charset=utf-8. The rewrite is GET-only, as documented. |
Web Worker API and Node vm API are not available | typeof Worker is undefined, construction throws ReferenceError. import("node:vm") fails with NotCapable: Requires run access. |
The static_files paths in config.toml are relative to the supabase/
directory (./functions/<name>/static/*).7 A glob rooted at
./supabase/functions/... matched nothing and shipped a 654-byte bundle with
no warning, which is the second silent way a static asset goes missing.
Runtime ceilings
Section titled “Runtime ceilings”Invocation-time limits, and a separate conversation from anything about deploys.1
| Ceiling | Documented | Measured |
|---|---|---|
| Memory | 256 MB | 64 MB allocation 200; 400 MB -> 546 {"code":"WORKER_RESOURCE_LIMIT","message":"Function failed due to not having enough compute resources (please check logs)"} |
| CPU time per request (excluding async I/O) | 2 s | 500 ms busy loop 200; 3 s -> 546 WORKER_RESOURCE_LIMIT with the body quoted in the Memory row |
| Wall clock | 150 s Free, 400 s paid; the docs define it as how long a worker stays active | a single stream on a fresh worker, ticking every 5 s and never idle, delivered its last tick at 395 s on a paid project and was cut between 395 s and 400 s; the client saw a truncated body, not an HTTP error, because the headers had long since been sent. Because the figure is a worker lifetime, a warm worker serving several requests shares it and a request that starts late in that life is cut sooner into itself; the shared case was not run. Free not run |
| Request idle timeout | 150 s, then 504 | 504 IDLE_TIMEOUT at 150 s, measured in incident resilience, class 10 |
| Recursive or nested calls | ~5000 requests per minute | did not bite: a self-calling function ran 59,562 depth-2 chains in 64.6 s, 110,607 nested calls per minute as the module recorded it, on a Micro project; 27 refusals (13 outer, 14 inner) across 119,124 nested calls and about 178,700 invocations in all, 0.02 per cent, answered 429 {"code":"RATE_LIMIT_EXCEEDED","message":"Too many requests. Re-try the request in 1 seconds."}; nothing else refused. A cap of 5,000 per minute would have refused most of the run |
| Log message length | 10,000 characters | a 12,019-character console.log line was stored as its first 10,000 characters followed by ....[truncated]; the invocation answered 200 |
| Log event threshold | 100 events per 10 seconds | 150 console.log calls inside one invocation: the first 100 were stored (indices 000 to 099), the last 50 dropped; the invocation answered 200 (the burst was under 10 s and inside one invocation, so per-invocation vs per-10 s is not separated) |
CPU and memory exhaustion return the same code and the same message, so the response does not say which one was hit; the function logs do. Long-running work does not fit inside an Edge Function on any plan.
The two log limits reject nothing. The line length bit at exactly the
documented figure; the event count did too, though the run cannot say whether
the window is ten seconds or one invocation. A function that exceeds them
keeps deploying and keeps answering; its
logs are cut instead, a long line to 10,000 characters plus an explicit
marker and a burst to its first 100 events, which is why they arrive as “we
hit a limit” from someone reading a dashboard rather than from a failed
deploy. The lines were read back through the Management API logs endpoint
(source = 'function_logs'), which needs an explicit iso_timestamp_start
and iso_timestamp_end or answers its generic backend error.
What to do about each ceiling
Section titled “What to do about each ceiling”The measurements above imply a short list of practices. Each row names the measurement it rests on; a row with no module id is a design choice rather than a result.
| Ceiling or failure | Practice | Rests on |
|---|---|---|
| Functions per project | Read function.max_count on the organization before quoting the plan figure. For per-tenant logic, one shared function with a routing table beats one function per tenant: a routing row can be ejected without a redeploy and the function count stays flat. | EF02; edge-resilience W25 (routing-row eject) |
| Bundle over 5 MB | Deploy through the CLI with Docker present, which bundles locally under the 20 MB ceiling. A platform that deploys programmatically should run the CLI in its pipeline for large bundles; the multipart endpoint and --use-api are the 5 MB path by construction. | EF04 |
| A 413 | Reproduce the same bundle serially before calling it a size limit. Both ceilings return the same body, and a serial 413 is a real size rejection. | EF04, EF05 |
| Secrets count | Keep per-tenant configuration out of Edge Function secrets. Hold it in a table, in Vault for anything sensitive (it stores ciphertext), and read it by tenant id at request time. Count user secrets, not the platform rows the list also shows. | EF03; security-lockdown S07 |
| Secret value size | The ceiling counts characters, so a base64 blob of up to 24,576 characters fits regardless of byte size. Split anything larger or move it to Vault. | EF03 |
| Deploys that report success | After every deploy round, list the functions or GET each slug and redeploy the difference. Treat CLI exit 0 as “the request was sent”. Lower concurrency: nothing in the battery lost a serial deploy. | EF05, EF11 |
| Same-slug deploys | Serialise deploys of one slug. A 409 is the collision signature; the losing deploy can be retried, and versions stayed monotonic through 8 rounds. | EF05, EF11 |
| Invocation 429 | Retry after the 1 s the body asks for; at 0.02 per cent of calls it is a rate smoother rather than a cap. Keep it separate from the control-plane ThrottlerException on deploys, which needs spacing between calls rather than a retry loop. | EF10; rate-limits L01 |
| CPU or memory 546 | The response does not say which was hit; read the function logs. Move CPU-bound or memory-heavy work out of the request path. | EF07 |
| Wall clock | Streaming keeps a request alive past the idle timeout but not past the worker’s 400 s. Chunk long work below that and hand anything longer to a queue with a consumer outside Edge Functions. | EF09; edge-resilience W13 |
| Log limits | Keep a log line under 10,000 characters and a burst under 100 events. Log one structured line per request rather than many, and sample high-volume paths; nothing warns when the cut happens. | EF08 |
| Image processing | Do not import sharp: it bundles, deploys and dies at invocation. Use Storage image transformations or a runtime that supports native threads. | EF06; image transformations billing |
| Static files | Deploy through the CLI with static_files in config.toml, paths relative to the supabase/ directory. The API path accepts the deploy and loses the asset. Serving the asset from Storage avoids the question. | EF06 |
| Outbound SMTP | Design for ports 25 and 587 being closed even though 587 connected in two runs: use an HTTP email API. | EF06 |
| HTML responses | Put the function behind a custom domain; on the project hostname a GET’s text/html is rewritten to text/plain. | EF06 |
Where the docs disagree with runtime
Section titled “Where the docs disagree with runtime”| Doc claim | Runtime measured | Severity |
|---|---|---|
| ports 25 and 587 blocked | 25 hangs; 587 connects (two runs); 465 connects | docs overstate |
| static files cannot be deployed via the API | API deploy with static_patterns answers 201; asset missing at runtime | docs understate: it fails silently |
| multithreaded libraries unsupported | sharp bundles and deploys; dies at invocation | deploy-time check absent |
| value ceiling stated as 48 KiB and as 24,576 characters | counts characters; 73,728 bytes accepted | docs conflate two units |
| the multipart deploy endpoint declares 201/401/402/403/429/500 | it also returns 413 on an oversized bundle | undeclared response; a client generated from the schema surfaces it only as an unknown status |
| 100 secrets per project | 100 user secrets; the 7 platform SUPABASE_* rows the list also shows do not count | docs silent |
| recursive or nested calls capped at ~5000 requests per minute | 110,607 nested calls per minute ran for a minute with 27 429 RATE_LIMIT_EXCEEDED refusals, 0.02 per cent, and no other ceiling | docs figure did not bite at 22x; whether such a cap counts per chain or per project the run cannot separate |
| wall clock 400 s on paid plans, defined per worker | one stream on a fresh worker: last tick at 395 s, cut between 395 s and 400 s (5 s tick) | holds for a single request; the shared-worker case not run |
| log message 10,000 characters; 100 events per 10 s | 10,000 kept plus a ....[truncated] marker; first 100 of 150 kept | figures hold; the 10 s window was not isolated from a per-invocation count |
| size ceilings 5 MB / 20 MB | both refuse with 413 request entity too large | docs silent on the error shape |
None of the docs disagreements on this page has been filed with Supabase as of 2026-09-02.
Reading the numbers
Section titled “Reading the numbers”- One project, one region, one day. The silent-loss counts (10 of 24, 9 of 24) describe that run’s control-plane conditions; the shape - success reported, function absent, no throttle visible - is the finding, the ratio is not a rate to plan against.
- The 8 CLI processes ran with
--use-apiso that eight local Docker bundles did not contend for one machine. The public issue ran the CLI’s default and reports the same shape.5 - Port reachability was a TCP connect from inside the function to a public SMTP host, with an 8 s timeout. A hang is recorded as blocked; a completed handshake as open. Nothing was sent on the socket.
- Boundary probes send N then N+1 and record both statuses. A pass means the platform accepted N and refused N+1; the docs figure is what N was set to.
- The size sources are random base64 in a string literal. A repeated character measures the bundler’s compressor rather than the ceiling.
- The second-wave rows (log limits, active wall clock, the recursive cap, the repeated races) come from a second project the same afternoon. The recursion rate is what 100 concurrent clients achieved against a Micro project, about 1,800 nested calls per second averaged over the run (59,562 chains in 65 s, about 916 chains per second); a larger compute tier or more clients would push it higher, and the cap still did not appear at this rate. The wall-clock cut is one run on one paid project.
Evidence, by module
Section titled “Evidence, by module”Measured claims map to module ids in experiments/edge-function-limits; the run record is its RUNLOG.md, and the redacted run artifacts with their measurement tables are published under out/2026-09-02/. Every number in a measured row below was taken from those tables. Documented rows rest on the cited pages only.
| Claim | Status | How it was checked |
|---|---|---|
| Secrets limits declared in the published OpenAPI schema match the docs; deploy endpoint declares 429, not 413 | measured | EF01 - the published OpenAPI document parsed and compared to pinned docs constants |
function.max_count 100 / 1000 / 2000 on Free / Pro / Team; function.size_limit_mb 20 on all three | measured | EF02 - entitlements read per organization, plan from the organization endpoint |
| All four secrets limits bite at N+1 with the errors quoted above; value counts characters; platform rows excluded from the 100 | measured | EF03 - N and N+1 posted per limit, list read between steps, cleanup to zero |
API refuses 8 MB with 413; CLI --use-api the same; CLI default lands 8 MB and refuses 24 MB with 413 | measured | EF04 - random-payload sources, each acceptance proven by GET plus invoke returning the byte count |
| 24 API deploys 8-wide: 24 x 201, 10 present; 8 CLI processes: 8 x exit 0, 9 present; no 429 | measured | EF05 - per-slug GET after the run, cleanup listing minutes later as the second read |
| Same-slug race: 201 x3, 409 x3, versions monotonic, function healthy; one delete 150 ms into a deploy left the function ACTIVE (first wave, superseded by the EF11 rows) | measured | EF05 - three rounds of two concurrent deploys; one delete 150 ms into a deploy; first project |
| HTML rewrite GET-only; 25 hangs, 587 and 465 connect; Worker and node:vm unavailable; static via API 201 then missing; static via CLI works; sharp 500s at run time | measured | EF06 - one function per restriction, responses recorded verbatim, two runs for ports and static |
CPU 3 s and memory 400 MB both 546 WORKER_RESOURCE_LIMIT | measured | EF07 - busy loop and allocation function with in-limit controls |
150 s idle timeout -> 504 IDLE_TIMEOUT; cold start shape | measured elsewhere | edge-resilience W13 / W18, cited above |
| Log line kept to 10,000 characters plus a marker; first 100 of 150 burst events kept | measured | EF08 - console.log from a function, read back via the logs endpoint (source = 'function_logs', time window required); second project |
| A single stream on a fresh worker, last tick at 395 s, cut before 400 s on a paid project | measured | EF09 - a stream ticking every 5 s asked for 450 s; second project; the shared-worker case not run |
110,607 nested calls per minute with 27 429 RATE_LIMIT_EXCEEDED (0.02 per cent) and no cap | measured | EF10 - one minute of depth-2 self-calls at concurrency 100; second project |
| Delete-during-deploy x10 and 4-wide same-slug x5: no corruption signature | measured | EF11 - GET and invoke after every round, a redeploy after each delete round; second project |
| Wall clock 150 s on Free | documented | not run; only a paid project was used |
| 138 of 237 functions absent after exit 0 at 8 parallel CLI processes | reported | public supabase/cli issue, not reproduced at that scale here |
Related docs
Section titled “Related docs”- Supabase incident resilience - the invocation-time ceilings (idle timeout, cold start) and every other incident class a client can work around.
- Where should a tenant live - the placement decision behind “one secret per tenant” and “one function per tenant”.
- Data surface lockdown - what
verify_jwton an Edge Function does and does not gate.
References
Section titled “References”-
Supabase, “Limits,” Supabase Docs. https://supabase.com/docs/guides/functions/limits ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Supabase, “Edge Functions: Deploy More Functions at No Extra Cost,” Supabase Changelog, 2024-07-18. https://supabase.com/changelog/28062-edge-functions-deploy-more-functions-at-no-extra-cost ↩
-
Supabase, “docs(functions): update Pro and Team function limits (#49173),” supabase/supabase commit 7107a22, 2026-08-18. https://github.com/supabase/supabase/commit/7107a22a6707c06546ee6d62aedb77da9c75b16a ↩
-
Supabase, “Management API OpenAPI document,” api.supabase.com, as served on 2026-09-02 (the document changes without notice). https://api.supabase.com/api/v1-json ↩ ↩2 ↩3
-
supabase/cli, “functions deploy exits 0 without deploying under concurrent load (138 of 237 functions missing),” GitHub issue #6247, 2026-08-18. https://github.com/supabase/cli/issues/6247 ↩ ↩2
-
Supabase, “Management API - Rate limits,” Supabase Docs. https://supabase.com/docs/reference/api/introduction ↩
-
Supabase, “CLI configuration -
functions.[function_name].static_files,” Supabase Docs. https://supabase.com/docs/guides/local-development/cli/config ↩