Skip to content

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.

LimitDefaultSettingOver it
Request body8 MiB (8,388,608 bytes)request.max_body_bytes413, PAYLOAD_TOO_LARGE
Query document length64 KiB (65,536 bytes)request.max_document_bytes400, DOCUMENT_SIZE_LIMIT_EXCEEDED
Selections per operation4,000request.max_selections400, DOCUMENT_SELECTION_LIMIT_EXCEEDED
Query depth10 levelsrequest.max_query_depth400, QUERY_DEPTH_LIMIT_EXCEEDED
IntrospectionRefusedrequest.introspection403, INTROSPECTION_DISABLED
JSON nesting in a request body128 levels400, INVALID_GRAPHQL_REQUEST
Requests in flight, requests per secondNo 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.

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" }
}
]
}
RuleDetail
Checked on every requestA cache hit included.
What is measuredThe bytes the edge received, not the Content-Length the client claimed.
What the caller learnsOnly that it sent too much. The limit and the refused size are in your logs (REQUEST_BODY_TOO_LARGE, or CLIENT_BODY_TOO_LARGE for a client profile’s max_body).
Changing itRaise or lower it with an integer. The limit cannot be removed: see request.max_body_bytes.
Why 8 MiBThe 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).

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.

SettingDefaultCheckedRefused with
request.max_document_bytes65536 (64 KiB)The length of the query string, before it is parsedDOCUMENT_SIZE_LIMIT_EXCEEDED
request.max_selections4000The number of selections, on the parsed document, before the query plan is builtDOCUMENT_SELECTION_LIMIT_EXCEEDED

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.

RuleDetail
Every field countsAt every nesting level, __typename included. An alias is a field of its own, so a: me { id } b: me { id } is four selections.
Fragment spreadsA 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 definitionsEvery 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 operationsOnly the operation the request runs counts.
@skip, @includeNot evaluated. A skipped selection is still planned, so it still counts.

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.

CaseDetail
Every parsed documentIn 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 documentsHeld to persisted_query.max_document_bytes and persisted_query.max_selections, which inherit the request.* values unless set. See [persisted_query].
An APQ registration over the byte ceilingNot stored, and refused with DOCUMENT_SIZE_LIMIT_EXCEEDED. A registration is written only once the request has cleared every other ceiling.
A cache hitNot checked, because a hit parses nothing. Refusals are never cached.
A persisted query pushThe 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.

Ten levels is past what ordinary product queries reach and short of what a nesting attack needs.

RuleDetail
Two ceilingsrequest.max_query_depth.client_document for documents clients send, registered_document for documents from the registry. 0 disables either.
How it is countedSelection sets from the operation’s own, deepest single path; fragments flattened, type conditions not a level. See how depth is counted.
IntrospectionExempt: 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 parseRefused with 400 and INVALID_GRAPHQL_REQUEST, never forwarded unmeasured.
GraphPilot’s own ceiling128 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_DEPTH_LIMIT_EXCEEDED.
A cache hitNot checked. See the GraphQL layer runs only on a miss.

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.

LimitFastly Compute
URL length8,192 bytes
Request header length128 KB per header
Response header length128 KB per header
Cached object size100 MB
Compute time50 ms CPU, 2 min wall clock per request
Memory128 MB heap, 1 MB stack per request
Backend requests32 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.

ResponseLimit
Larger than the cached object sizeDelivered to the client, not stored.
Compressed by your originDecoded (gzip, deflate, br, zstd). A body GraphPilot cannot decode, or one that would decode to more than it processes, is answered with 502 (ORIGIN_RESPONSE_ENCODING_UNSUPPORTED, ORIGIN_RESPONSE_ENCODING_CORRUPT, ORIGIN_RESPONSE_DECODED_TOO_LARGE). See compression.
A GraphQL response over 133 MiB (139,460,608 bytes), or carrying an integer larger than 18446744073709551615Not parsed, so delivered unchanged and not stored, with CACHE_SKIPPED_UNPARSEABLE_GRAPHQL_RESPONSE. See refusals that depend on the response.
StreamedNot supported. A response is delivered once it is complete, so Server-Sent Events, multipart/mixed and incremental delivery with @defer or @stream do not work through the edge.
A total time at the edge over 120 secondsRefused at deploy time: every attempt the retry policy allows has to fit. See timeouts and retries.
More than 32 backend requests in one requestEvery 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.retry.max_retries above 30 is not refused at deploy time, but its later attempts cannot be sent.
LimitValue
Cached object size100 MB (Fastly). A larger response is delivered, not stored.
Surrogate keys per response16,000 bytes in total. See below.
gp-surrogate-key header8,192 bytes by default, 1 to 128000. See size limit.
Surrogate key length for a purge1024 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 call1000, 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 purgeByQueryHash1000 entries in queries. A longer list is refused with BAD_REQUEST before any entry is resolved.
PurgesNo rate limit and no cap on calls.

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.

RuleDetail
No key is ever droppedIf the keys do not fit, the response is not cached: CACHE_SKIPPED_SURROGATE_KEYS_TOO_LARGE. An entry stored without some of its keys could never be reached by a purge.
CollisionsTwo 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 countA 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.ignored_types and ignored_fields.
Inherited keys count doubleA 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 listA response with thousands of keys is usually a long list; paginating it keeps each page under the limit.
LimitValueOver it
Operation document size32 KiB (32,768 bytes)400, OPERATION_LIMIT_EXCEEDED
Operation tokens5,000400, OPERATION_LIMIT_EXCEEDED
Operation depth15 levels400, OPERATION_LIMIT_EXCEEDED
Operation selections500400, OPERATION_LIMIT_EXCEEDED
Operation aliases30400, OPERATION_LIMIT_EXCEEDED
WebSocket message131,072 bytesConnection closed with 1009
pushPersistedQueries documents per call100BAD_USER_INPUT; nothing is applied
Persisted query document size100,000 bytesBAD_USER_INPUT; nothing is applied
removePersistedQueries hashes per call1000, the edge’s own limit on purge keys in one requestBAD_USER_INPUT; nothing is applied
Service.clientRegisteredQueries pageDefault 50, at most 100Clamped
Purge keys per call1000BAD_REQUEST
Service.purges pageDefault 20, at most 1000Clamped
Service.requests, operations, errors, clients pageDefault 20, at most 1000Clamped
Service.logs pageDefault 100, at most 1000Clamped
Time seriesAt most 400 buckets. A finer granularity is coarsened until the window fits.Coarsened
Top lists (top)Default 10, at most 100 entriesClamped
Series in a grouped time series (topGroups)Default 10, at most 50; the rest is summed into one other seriesClamped
Time range of a telemetry queryClipped to what your plan retains. See Insights.Clipped
serviceRequestsStream, serviceOperationsStreamClosed after 5 minutes without a new entryResubscribe
serviceLogsStreamClosed after 30 minutesResubscribe
Confirmation codes requested3 per user per 60 seconds. A code expires after 10 minutes.TOO_MANY_REQUESTS
Attempts per confirmation code5, a correct attempt included. Requesting a new code replaces the earlier one.INVALID_CONFIRMATION_CODE
API key validationCached for up to 60 seconds, so a revoked key can work for up to a minute
Unrecognized API keys checked per client address10, then one more every 6 seconds. See API key validation.429, TOO_MANY_REQUESTS, with Retry-After
Unrecognized API keys checked across all clients20, then two more per second. See API key validation.429, TOO_MANY_REQUESTS, 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.

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.

RuleDetail
SizeBytes of the query string, UTF-8.
DepthFields that carry a selection set, through fragments. Introspection (__schema, __type) is not held to it.
SelectionsEvery 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.
AliasesCounted as written in the document.
RefusalOver HTTP a 400; over a WebSocket the error for that operation. extensions.code is OPERATION_LIMIT_EXCEEDED, extensions.limit names the limit (bytes, tokens, depth, selections or aliases) and extensions.max its value.

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 429 with TOO_MANY_REQUESTS and Retry-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.