Connection to the origin
Every request the cache cannot answer goes on to your origin. This page covers that connection: scheme and port, what GraphPilot asks your origin to send, how long it waits, and when it tries again. Whether your origin can trust what arrives is on request signing and the origin allowlist.
TLS and ports
Section titled “TLS and ports”GraphPilot speaks TLS to your origin when your origin URL is https://, and does not when it
is http://. There is no separate switch: the scheme you register the service with is the
scheme GraphPilot uses.
On an https:// origin the connection is verified: the certificate is checked against your
origin’s hostname, and SNI is sent with that hostname. A certificate that does not match is a
failed connection, not a warning.
An http:// origin is not encrypted, and the signature does not
compensate: it proves who sent the request, not that nobody read it. If your origin is reachable
over the public internet, register it as https://.
The origin URL belongs to the service, not to gpilot.toml. Change it in the portal or with
gpilot service edit; it takes effect with the next deployment. There
is no mutual TLS to the origin today.
Your origin receives its own hostname in Host, the host of the origin URL, not the
hostname the client called (a custom domain or the service’s
GraphPilot hostname). An origin that routes by Host, such as a virtual host or a platform that
serves several apps from one address, has to answer on the hostname in the origin URL.
What else your origin receives
Section titled “What else your origin receives”These options under [origin] change the request on its way to your origin. Each is off or
neutral by default:
| Option | What it does |
|---|---|
origin. | Rewrites QUERY to POST, or POST to QUERY for query operations. |
[origin. and [origin. | Header and cookie names removed before the request reaches your origin. |
origin. | Sends the GraphPilot request id under a header name you choose. |
origin. | Sends the client’s IP address in gp-client-ip. |
The full list of request headers your origin sees, and which ones GraphPilot adds or removes, is on request headers your origin receives.
Compression
Section titled “Compression”GraphPilot sends your origin Accept-Encoding: identity, asking for an uncompressed body. It
has to read the body to derive surrogate keys, to remove the fields it injected and to decide
whether to cache it, and none of that works on compressed bytes. Compression toward the client
is added on the way out, negotiated against the client’s own Accept-Encoding, so your clients
still receive compressed responses.
An origin that compresses anyway still works, at a cost: a body encoded with gzip, deflate,
br or zstd is decoded first, logged as ORIGIN_RESPONSE_DECODED with both sizes. A body that
cannot be decoded is answered with 502 Bad Gateway:
| Code | When |
|---|---|
ORIGIN_ | The Content- names a coding GraphPilot cannot decode. |
ORIGIN_ | The body did not decode under the coding its Content- named. |
ORIGIN_ | The body would decode to more than GraphPilot processes. |
The fix for all three is to make your origin honor Accept-Encoding.
A plain HTTP response body reaches your client byte-identical to what your origin sent, after any decoding above. GraphPilot does not rewrite it, so a signature, a digest or an ETag your origin computed over the body stays valid.
Timeouts
Section titled “Timeouts”Three optional keys under [origin] in gpilot.toml bound how long the edge waits for your
origin:
| Key | What it bounds | Default | Ceiling |
|---|---|---|---|
connect_ | Accepting the TCP connection and completing the TLS handshake | 1000 | 5000 on every plan |
first_ | Waiting for the first byte of the response, after the request was sent | 15000, or your plan’s ceiling if that is lower | Per plan |
between_ | Waiting between two bytes of a response that has already started | 10000 | 60000 on every plan |
They take effect with your next deployment. An origin that does not connect or send its first
byte in time fails the request with 502 and BAD_GATEWAY, unless a retry succeeds.
What your plan allows
Section titled “What your plan allows”| Plan | first_ ceiling |
|---|---|
| Glider (Free) | 5000 |
| Jet (Starter) | 15000 |
| Airliner (Business) | 30000 |
| Jumbo (Enterprise) | 30000 |
A ceiling is not a recommendation: failing in two seconds is often better than failing in thirty.
On Glider (Free), the default is lower. An unconfigured service gets its plan’s ceiling where
that is below 15000, so the edge waits 5 seconds for the first byte. A container scaling up
from zero can take longer, and the request fails with a 502 while it boots. Keep an instance
warm, or move to a plan whose ceiling covers your cold start.
A value out of bounds is refused
Section titled “A value out of bounds is refused”Nothing is quietly adjusted. The deployment is refused, with a message naming the value, when:
-
a value exceeds its ceiling. The message names the ceiling and the plan:
origin.first_byte_timeout_ms = 30000 exceeds the maximum of 5000 ms on the FREE plan.Lower the value or upgrade the plan.After a downgrade, your next deployment is refused until the value comes down.
-
a value is below 100 ms. The values are milliseconds, and a smaller one is almost certainly seconds written in the wrong unit:
origin.first_byte_timeout_ms = 30 is below the minimum of 100 ms. The value is milliseconds;30 would fail every request to a healthy origin.
Timeouts and retries
Section titled “Timeouts and retries”A request may spend at most 120 seconds at the edge: the wall clock Fastly’s edge network, which GraphPilot runs on, allows per request. With retries, every attempt gets the full connect and first-byte timeouts, and the delays between attempts add up, so the worst case has to fit:
(max_retries + 1) × (connect_timeout_ms + first_byte_timeout_ms) + backoff delays ≤ 120000 msA configuration that does not fit is refused by the deployment build, with a message naming the
worst case and asking you to lower origin.first_byte_timeout_ms or origin.retry.max_retries.
between_bytes_timeout_ms is not counted, because how long a body takes depends on its size.
This is also why no plan allows a first-byte timeout above 30000: a retry could never run.
The edge also allows each request at most 32 backend requests,
and every attempt is one of them. Keep max_retries at 30 or below: the deployment build checks
the time budget only, so a higher value is accepted, and the attempts past the limit cannot be
sent.
Fitting the budget is not fitting your client’s patience: two attempts at 30 seconds are a minute of waiting. Pick the numbers together.
Retries
Section titled “Retries”A request that fails before it reaches your application can be sent again without the client learning that it failed. GraphPilot does this only when you ask it to.
The keys, under [origin.retry]:
| Key | Type | Default | What it does |
|---|---|---|---|
enabled | boolean | false | Whether failed origin requests are retried at all. |
max_retries | integer | 1 | How many additional attempts a failed request gets. 1 means at most two attempts in total. |
status_codes | list of integers | [502, 503, 504] | The origin response statuses that trigger a retry. |
on_ | boolean | true | Whether a transport failure is retried. |
retry_ | boolean | false | Whether GraphQL mutations are retried. Does not apply to plain HTTP requests. |
retry_ | boolean | false | Whether plain HTTP requests with a method other than GET, HEAD or OPTIONS are retried. Does not apply to GraphQL requests. |
backoff | enum | "fixed" | How the delay between attempts is computed. |
delay_ms | integer | 1000 | The base delay in milliseconds. |
max_delay_ms | integer | 2000 | The ceiling on any single delay, for both fixed and exponential. |
What counts as a failure
Section titled “What counts as a failure”A retryable status. The default list is the three gateway statuses that mean the request did
not reach your application. A 500 is deliberately not in it: a 500 is your application
answering, and answering twice is rarely what a client wants. If your origin uses 5xx
semantically, add to the list rather than assuming.
A transport failure, covered by on_network_error. The origin was unreachable, the
connection dropped, or a timeout expired. There is no status to match on, because
nothing came back at all.
Which requests are repeated
Section titled “Which requests are repeated”GraphQL mutations are not retried by default. Enabling [origin.retry] repeats queries; a
mutation is sent once unless you set retry_mutations = true. The retryable statuses usually mean
the request never reached your application, so the mutation never ran, but that holds only as far
as your origin’s status codes do. A proxy in front of your application can time out with a 504
while the application is still committing, and the write would then run twice.
A timeout says nothing about whether your origin already acted. With on_network_error on (the
default once retries are enabled), a transport failure is retried, a timeout
included. With retry_mutations = true, that covers mutations too, so a mutation that timed out
after your origin executed it is sent again. Set retry_mutations = true only if your origin
answers a gateway status before it executes anything, or your mutations are idempotent, for
example by an idempotency key.
A plain HTTP request is retried only when its method is GET, HEAD or OPTIONS. A
POST, PUT, PATCH, DELETE or any other method is sent once, because repeating a write your
origin may already have executed would execute it twice. Set retry_unsafe_methods = true only
for an origin that makes its writes idempotent, for example by an idempotency key. The two
switches do not overlap: retry_mutations never decides a plain HTTP request, and
retry_unsafe_methods never decides a GraphQL one.
A WebSocket handshake is never retried, whatever the configuration says. The connection is
handed to your origin on the first attempt, so there is no second attempt to make; a handshake
that fails reaches the client as a 502. See subscriptions and
WebSockets.
Backoff
Section titled “Backoff”| Strategy | Behavior | When to use it |
|---|---|---|
none | Retry immediately. | Your origin is a pool and a failure means “this instance is unhealthy”. The next attempt lands elsewhere, and waiting only adds latency. |
fixed (default) | Wait delay_ms before every retry, capped at max_delay_ms. | The general case. |
exponential | Wait delay_ms, then double before each further attempt, capped at max_delay_ms. | The failure is more likely load than one bad instance, so attempts spread out instead of arriving together. |
A delay is spent suspended, so it costs wall clock, not CPU, and counts against the 120-second budget.
Observing them
Section titled “Observing them”A response whose origin request was repeated carries the count, and the header is absent when there were none:
gp-origin-retries: 2The same event is logged as ORIGIN_RETRIED, once per repeated attempt, which is what to alert
on: retries succeeding quietly still mean your origin is failing.
A retry happens below the cache: the response that eventually succeeds goes through the normal store decision, and a cache hit never retries. A background revalidation is an ordinary origin request and retries like any other.
Shielding
Section titled “Shielding”With shielding, a miss at an edge location goes to the shield location first, and only the shield connects to your origin. The shield’s request to your origin uses the same TLS, signing and timeouts as any other.
The extra hop counts against the 120-second budget. The edge waits
for the shield’s whole origin sequence, every retry attempt and the gaps between them, plus a
fixed 5-second hop buffer, rather than a single attempt’s timeout: the shield may still be
legitimately retrying when a single attempt’s timeout would have expired. An unset
first_byte_timeout_ms counts as 15000 ms here. A configuration whose total would exceed the
120-second budget is refused by the deployment build, naming origin.shield.
Retries do not double at your origin. With a shield, your origin sees no more attempts per
request than your [origin.retry] policy allows, the same as without one.
When the shield cannot be used, a read falls back to your origin and a mutation is refused; how each case is answered is on when the shield cannot be used, and how a shielded request counts for usage on request units.
Related
Section titled “Related”- Request signing: proving to your origin that a request came through GraphPilot.
- Origin allowlist: the addresses GraphPilot connects from.
- Shielding
- Quotas and lean mode
- Configuration reference
- Troubleshooting