Limits
What each plan includes is on quotas (request units, services, custom domains) and on the pricing page (the numbers per plan), and the origin timeout ceilings are on what your plan allows. This page lists the technical limits.
Request limits
Section titled “Request limits”| Limit | Default | Setting | Over it |
|---|---|---|---|
| Request body | 8 MiB (8,388,608 bytes) | request. | 413, PAYLOAD_ |
| Query document length | 64 KiB (65,536 bytes) | request. | 400, DOCUMENT_ |
| Selections per operation | 4,000 | request. | 400, DOCUMENT_ |
| Query depth | 10 levels | request. | 400, QUERY_ |
| Introspection | Refused | request. | 403, INTROSPECTION_ |
| JSON nesting in a request body | 128 levels | 400, INVALID_ | |
| Requests in flight, requests per second | No limit |
Every limit is inclusive: a request exactly at the limit is served. A client profile can set a second, lower ceiling for each of the first four; the stricter of the two decides. The document, selection and depth limits and the introspection block apply to a parsed GraphQL operation only.
These limits apply to HTTP requests that reach the edge. Messages inside a WebSocket are not checked against any of them, and there are no separate limits on aliases, directives, tokens or query cost. See what the edge does not check for what to enforce at your origin instead.
Request body
Section titled “Request body”A body over the limit is refused before it is parsed and before it reaches your origin:
{ "errors": [ { "message": "The request body is too large.", "extensions": { "code": "PAYLOAD_TOO_LARGE" } } ]}| Rule | Detail |
|---|---|
| Checked on every request | A cache hit included. |
| What is measured | The bytes the edge received, not the Content- the client claimed. |
| What the caller learns | Only that it sent too much. The limit and the refused size are in your logs (REQUEST_, or CLIENT_ for a client profile’s max_body). |
| Changing it | Raise or lower it with an integer. The limit cannot be removed: see request.. |
| Why 8 MiB | The body is parsed into a JSON tree and rewritten, so a large variables object costs several times its size in memory. 8 MiB leaves a request room to be answered inside what one edge execution may spend (128 MB of memory, see edge platform limits). |
Query document
Section titled “Query document”The cost of a request is dominated by the document, not by the payload around it. A large
variables object is parsed once and passed on; every field selection is parsed, planned and
printed back out, so a document of thousands of aliased selections costs far more than its size
suggests. The body limit does not bound that cost, so the document has two limits of its own.
| Setting | Default | Checked | Refused with |
|---|---|---|---|
request. | 65536 (64 KiB) | The length of the query string, before it is parsed | DOCUMENT_ |
request. | 4000 | The number of selections, on the parsed document, before the query plan is built | DOCUMENT_ |
0 turns either limit off. 64 KiB is past any document a product ships, hand-written or
generated from colocated fragments. 4,000 selections is past what an ordinary product page
selects in one operation, and the standard getIntrospectionQuery() document is served under both
defaults.
How selections are counted
Section titled “How selections are counted”| Rule | Detail |
|---|---|
| Every field counts | At every nesting level, __typename included. An alias is a field of its own, so a: me { id } b: me { id } is four selections. |
| Fragment spreads | A spread counts the fragment’s fields every time it is spread: a fragment of 50 fields spread in three places is 150. Inline fragments and the spread itself add nothing. |
| Fragment definitions | Every definition is paid for. A fragment the operation never spreads still counts its own fields once, and so does a second definition of a name a later one replaces. |
| Other operations | Only the operation the request runs counts. |
@skip, @include | Not evaluated. A skipped selection is still planned, so it still counts. |
What a refusal looks like
Section titled “What a refusal looks like”A 400, before your origin is called, with the numbers in the message:
{ "errors": [ { "message": "The operation makes 6144 selections; this service allows 4000.", "extensions": { "code": "DOCUMENT_SELECTION_LIMIT_EXCEEDED" } } ]}DOCUMENT_SIZE_LIMIT_EXCEEDED reads The operation's document is <n> bytes long; this service allows <limit>. It is a 400 and not a 413 because the request itself may be tiny: a hash whose
registered document is too long is refused too. The log line carries the measured bytes or
selections and the limit. A document over a limit is refused, never forwarded unparsed.
Where the limits apply
Section titled “Where the limits apply”| Case | Detail |
|---|---|
| Every parsed document | In the request body, in an APQ registration, or resolved from the registry for a hash. A registered document costs exactly what the same text costs from anyone. |
| Registered documents | Held to persisted_query. and persisted_query., which inherit the request.* values unless set. See [persisted_. |
| An APQ registration over the byte ceiling | Not stored, and refused with DOCUMENT_. A registration is written only once the request has cleared every other ceiling. |
| A cache hit | Not checked, because a hit parses nothing. Refusals are never cached. |
| A persisted query push | The persisted query push checks each document against the service’s deployed ceilings (the defaults when the service has not deployed yet) and marks an oversized one REJECTED, without failing the rest of the batch. |
Query depth
Section titled “Query depth”Ten levels is past what ordinary product queries reach and short of what a nesting attack needs.
| Rule | Detail |
|---|---|
| Two ceilings | request. for documents clients send, registered_ for documents from the registry. 0 disables either. |
| How it is counted | Selection sets from the operation’s own, deepest single path; fragments flattened, type conditions not a level. See how depth is counted. |
| Introspection | Exempt: nothing under __schema is planned, so an introspection query measures one level however deeply it is written. A deeply nested one is not refused by this limit either. |
| A query that does not parse | Refused with 400 and INVALID_, never forwarded unmeasured. |
| GraphPilot’s own ceiling | 128 levels, whatever the setting says and even with it at 0: counted on the document’s brackets before it is parsed, and on its fields with fragments expanded before it is planned. Refused with QUERY_. |
| A cache hit | Not checked. See the GraphQL layer runs only on a miss. |
Edge platform limits
Section titled “Edge platform limits”Fastly’s own ceilings, from its Compute resource limits. They apply to every request, whatever GraphPilot is configured to do, and are a property of the platform GraphPilot runs on today, not of GraphPilot itself. They are the same on every plan, and no setting raises them.
Each limit counts per execution: one run of the edge program for one request at one edge location. A request through the shield runs twice, once at the edge location that received it and once at the shield, and each run is held to the limits below on its own. The wall clock of the run at the edge location includes the time it waits for the shield.
| Limit | Fastly Compute |
|---|---|
| URL length | 8,192 bytes |
| Request header length | 128 KB per header |
| Response header length | 128 KB per header |
| Cached object size | 100 MB |
| Compute time | 50 ms CPU, 2 min wall clock per request |
| Memory | 128 MB heap, 1 MB stack per request |
| Backend requests | 32 per request |
The URL length matters for GET requests, where the operation and its variables travel in the
query string. A persisted query sends a 64-character hash instead.
| Response | Limit |
|---|---|
| Larger than the cached object size | Delivered to the client, not stored. |
| Compressed by your origin | Decoded (gzip, deflate, br, zstd). A body GraphPilot cannot decode, or one that would decode to more than it processes, is answered with 502 (ORIGIN_, ORIGIN_, ORIGIN_). See compression. |
| A GraphQL response over 133 MiB (139,460,608 bytes), or carrying an integer larger than 18446744073709551615 | Not parsed, so delivered unchanged and not stored, with CACHE_. See refusals that depend on the response. |
| Streamed | Not supported. A response is delivered once it is complete, so Server-Sent Events, multipart/ and incremental delivery with @defer or @stream do not work through the edge. |
| A total time at the edge over 120 seconds | Refused at deploy time: every attempt the retry policy allows has to fit. See timeouts and retries. |
| More than 32 backend requests in one request | Every origin attempt is one backend request, and so is a fetch of an auth provider’s key set (jwks_url) and, at the edge location that received a shielded request, the hop to the shield. An origin. above 30 is not refused at deploy time, but its later attempts cannot be sent. |
Cache limits
Section titled “Cache limits”| Limit | Value |
|---|---|
| Cached object size | 100 MB (Fastly). A larger response is delivered, not stored. |
| Surrogate keys per response | 16,000 bytes in total. See below. |
gp- header | 8,192 bytes by default, 1 to 128000. See size limit. |
| Surrogate key length for a purge | 1024 characters (Unicode code points) as sent, and 1024 bytes once folded. A longer key is still attached to a response, but a purge naming it, or an empty key, is refused with BAD_REQUEST. |
| Keys per purge call | 1000, counted after deduplication, per soft and hard group, and after request ids and query hashes are expanded into keys. Over it, BAD_REQUEST. |
Query hashes per purgeByQueryHash | 1000 entries in queries. A longer list is refused with BAD_REQUEST before any entry is resolved. |
| Purges | No rate limit and no cap on calls. |
How many keys fit on one response
Section titled “How many keys fit on one response”The surrogate keys of one response are stored with the cache entry, and the edge allows them 16,000 bytes in total. GraphPilot stores each key as a short hash, so about 1,300 to 2,000 keys fit, depending on how many a response carries. The count includes the operation and cache keys GraphPilot adds to every response.
| Rule | Detail |
|---|---|
| No key is ever dropped | If the keys do not fit, the response is not cached: CACHE_. An entry stored without some of its keys could never be reached by a purge. |
| Collisions | Two keys can share a hash. A collision can only over-invalidate (a purge also removes the entries of the key sharing its hash), never leave stale data. |
| Keys that identify nothing still count | A field that merely happens to be named id, _id or key, or to have a key scalar type, adds a key per object. Switch the automatic rules off there with @surrogateKey(ignore: true) or surrogate_key. and ignored_. |
| Inherited keys count double | A key inherited from an interface or union adds up to two keys per object, one named after the abstract type and one after the object’s own type. |
| A long list | A response with thousands of keys is usually a long list; paginating it keeps each page under the limit. |
GraphPilot API limits
Section titled “GraphPilot API limits”| Limit | Value | Over it |
|---|---|---|
| Operation document size | 32 KiB (32,768 bytes) | 400, OPERATION_ |
| Operation tokens | 5,000 | 400, OPERATION_ |
| Operation depth | 15 levels | 400, OPERATION_ |
| Operation selections | 500 | 400, OPERATION_ |
| Operation aliases | 30 | 400, OPERATION_ |
| WebSocket message | 131,072 bytes | Connection closed with 1009 |
pushPersistedQueries documents per call | 100 | BAD_; nothing is applied |
| Persisted query document size | 100,000 bytes | BAD_; nothing is applied |
removePersistedQueries hashes per call | 1000, the edge’s own limit on purge keys in one request | BAD_; nothing is applied |
Service. page | Default 50, at most 100 | Clamped |
| Purge keys per call | 1000 | BAD_REQUEST |
Service. page | Default 20, at most 1000 | Clamped |
Service., operations, errors, clients page | Default 20, at most 1000 | Clamped |
Service.logs page | Default 100, at most 1000 | Clamped |
| Time series | At most 400 buckets. A finer granularity is coarsened until the window fits. | Coarsened |
Top lists (top) | Default 10, at most 100 entries | Clamped |
Series in a grouped time series (topGroups) | Default 10, at most 50; the rest is summed into one other series | Clamped |
| Time range of a telemetry query | Clipped to what your plan retains. See Insights. | Clipped |
serviceRequestsStream, serviceOperationsStream | Closed after 5 minutes without a new entry | Resubscribe |
serviceLogsStream | Closed after 30 minutes | Resubscribe |
| Confirmation codes requested | 3 per user per 60 seconds. A code expires after 10 minutes. | TOO_ |
| Attempts per confirmation code | 5, a correct attempt included. Requesting a new code replaces the earlier one. | INVALID_ |
| API key validation | Cached for up to 60 seconds, so a revoked key can work for up to a minute | |
| Unrecognized API keys checked per client address | 10, then one more every 6 seconds. See API key validation. | 429, TOO_, with Retry-After |
| Unrecognized API keys checked across all clients | 20, then two more per second. See API key validation. | 429, TOO_, with Retry-After |
The purge mutations have no rate limit of their own; an unrecognized key sent to them counts against the API key validation budget below like any other request.
Operation limits
Section titled “Operation limits”The API checks every operation document before it validates it, on HTTP and on a WebSocket alike. These limits are the API’s own and are separate from the request limits your service sets for its edge.
| Rule | Detail |
|---|---|
| Size | Bytes of the query string, UTF-8. |
| Depth | Fields that carry a selection set, through fragments. Introspection (__schema, __type) is not held to it. |
| Selections | Every field and every fragment spread, once per place it is reached: a fragment spread twenty times counts twenty times. A fragment no operation spreads counts too. |
| Aliases | Counted as written in the document. |
| Refusal | Over HTTP a 400; over a WebSocket the error for that operation. extensions. is OPERATION_, extensions. names the limit (bytes, tokens, depth, selections or aliases) and extensions. its value. |
API key validation
Section titled “API key validation”Each sk_ key the API has not accepted within the last minute is looked up with the identity
provider before the request runs. Those lookups are budgeted, so that a flood of invented keys
cannot crowd out real ones:
- A key the API accepted within the last minute is answered from its cache and never counts against the budget, however much else is going on.
- Every other well-formed key counts: a key whose acceptance is more than a minute old (it is
looked up again then, however often it is used), a new key on its first use, and every
unknown, revoked or mistyped one. Within the budget it is looked up as usual; over
it, the request is answered
429withTOO_MANY_REQUESTSandRetry-After, without the key being checked. - A client address is the one the edge saw. An IPv6 address counts by its
/64. - A key that was refused is remembered as refused for 10 seconds, so repeating it does not count again in that time.
- The numbers above are a floor for what one client can send, not an exact count.
A CI job that presents one key uses one lookup a minute at most. A 429 here means a client
has presented many different keys in a short time, or the API is answering a flood of them; send
the request again after Retry-After.