Skip to content

Shielding

Plans Jet (Starter) and above

Without shielding, each edge location holds its own cache: the first request for an entry at every location is a miss that goes to your origin. Shielding puts one edge location, the shield, between every other location and your origin. A miss anywhere asks the shield first, so the shield’s cache is in effect shared by the whole network and your origin sees about one miss per entry per lifetime, however many locations request it.

Shielding is off by default. It is worth switching on when your audience is spread across many regions, or when your origin is expensive to call; it is less useful when most traffic arrives at one or two locations anyway.

Name the shield location in the [origin] table of gpilot.toml and deploy:

[origin]
shield = "eu-frankfurt"
  • Pick the location closest to your origin. The shield is the only location that connects to your origin, so its distance to the origin is what every shielded miss pays. The values are in the shield location table; a value that is not in it refuses the deployment, and the refusal suggests the closest values when there are any.
  • Your plan decides whether the setting is used. Shielding is included in every paid plan. On Glider (Free) a configured shield still deploys, with a warning on origin.shield that names the location and says it is inactive on this plan and turns on by itself after an upgrade, with no redeploy needed. The edge checks the plan on every request, so an upgrade switches the shield on and a downgrade to Glider switches it off without a new deployment. While it is off, every miss goes to your origin directly.
  • The portal shows it. The service’s Config view names the shield’s city and country next to origin.shield, and says whether the shield is active on the plan. See schema and config findings.

To switch shielding off, remove shield and deploy again.

shop-api runs in a Frankfurt data center, and a third of its traffic comes from North America and Asia. Without a shield, each of those edge locations fills its own cache from Frankfurt. eu-frankfurt is the shield location next to the origin:

gpilot.toml
[origin]
shield = "eu-frankfurt"

A value from another vocabulary is refused before anything is deployed. The CDN’s own code for the same location, frankfurt-de, fails the deployment with:

unknown shield location "frankfurt-de", did you mean "eu-frankfurt"?

After the deployment, a miss at an edge location in Chicago asks the shield first. The request’s Overview tab in Insights shows the Shield set with Frankfurt as the location: the first request for an entry reads Result MISS there, and the same request from Singapore a moment later reads HIT, because the shield already holds the entry and your origin is not called. The Shield section of the Cache hit card sums this up for the time range; its Offload is the share of requests your origin never saw. Usage rises with it: a miss through the shield counts at two edge locations (see request units).

  1. A request arrives at the nearest edge location and is looked up in that location’s cache. A hit is answered there, exactly as without shielding.
  2. On a miss, the edge location sends the request to the shield instead of to your origin. It sends the request as the client sent it, signed so the shield can tell it came from a GraphPilot edge location and which client address it was for.
  3. The shield looks the entry up in its own cache, under the same cache key and the same partition. On a hit there, it answers and your origin is not contacted.
  4. On a miss at the shield, the shield calls your origin with the same TLS, request signing, timeouts and retries as any other origin request, and stores the response.
  5. The response travels back to the edge location, which stores its own copy and answers the client. Later requests at that location are answered from its own cache.

The edge location stores the shield’s response with what is left of its lifetime: the shield’s age is subtracted from the max-age, so both copies expire together. When nothing is left, the response is served but not stored at the edge location (CACHE_SKIPPED_SHIELD_ENTRY_EXHAUSTED). A response the shield answered from a stale copy, or without consulting its cache, is served but not stored at the edge location either (CACHE_SKIPPED_SHIELD_RESPONSE_NOT_STORABLE); see stale windows with shielding.

Misses from many locations meet again at the shield, so request collapsing works across locations too.

Only a cache miss goes through the shield, and only when the request is one of these:

RequestThrough the shield
A GraphQL queryYes
A GraphQL mutationYes, see mutations and purges
A GraphQL request carrying only a persisted query hashYes; the shield resolves the hash
A GraphQL document the edge location cannot parseYes; the shield refuses it the same way
A plain HTTP GET or HEADYes
A GraphQL subscription, any other plain HTTP method, a WebSocket upgradeNo, straight to your origin
A request that never consults the cache (a PASS with a CACHE_PASS_* code, for example one matching cache.policy.bypass)No, straight to your origin
A request served in lean modeNo, straight to your origin

A background refresh of a stale entry at an edge location also goes through the shield.

A mutation goes through the shield, so its automatic purge runs at the shield first and at the edge location after it, on the way back. Purging in that order keeps the edge location from refilling from a shield copy that is about to be purged.

A purge through the API, the CLI or the portal is sent twice on a shielded service, so an edge location purged before the shield cannot refill from the shield’s copy. A purge clears the entry at the shield too, because purges are global.

The edge location sends the shield exactly one request and does not retry it; retries run between the shield and your origin. What happens when the hop fails depends on the request:

FailureA query, GET or HEADA mutationA request whose kind the edge cannot see (only a hash, or a document it cannot parse)
The shield could not be reached before the request was sent (for example DNS, connect or TLS)Sent to your origin by the edge location (FALLBACK)502, nothing sent to your origin (UNAVAILABLE)Sent to your origin by the edge location (FALLBACK)
The send failed in a way that leaves unclear whether the shield received it, or the answer did not come from the shieldSent to your origin by the edge location (FALLBACK)502 (UNAVAILABLE)502 (UNAVAILABLE)
No usable answer within the time budget, or the connection broke off504 (TIMEOUT)504 (TIMEOUT)504 (TIMEOUT)
The shield answered with a body the edge location could not decode502502502
The shield answered, including with an error status from your originPassed through as it isPassed through as it isPassed through as it is

A mutation never falls back to your origin, because the edge location sending it itself could run it twice and would purge this location before the shield. A fallback is logged as SHIELD_UNAVAILABLE_FALLBACK, a refused mutation as SHIELD_UNAVAILABLE_NO_FALLBACK, and a timeout answers GATEWAY_TIMEOUT; see codes. The reason on a SHIELD_UNAVAILABLE_FALLBACK or SHIELD_UNAVAILABLE_NO_FALLBACK line names the step that failed; quote it to support.

While the shield cannot be reached, a service keeps working on queries at the cost of the per-location warm-up, as without shielding.

The time budget. The edge waits for the shield’s whole origin sequence (every attempt your [origin.retry] policy allows, and the gaps between them) plus a fixed 5 seconds for the hop, never more than the 120-second budget. An unset first_byte_timeout_ms counts as 15000 ms here. A configuration whose total exceeds the 120-second budget is refused by the deployment build, naming origin.shield. See connection to the origin.

  • What your clients see does not change. The response headers come from the edge location that answered the client: gp-cache says whether that location had the entry, so a response the shield supplied reads MISS there. See cache statuses.
  • The hop headers stay inside GraphPilot. The headers the shield and the edge location exchange never reach your client or your origin, and the same names are removed if your origin sends them (see reserved names).
  • Your origin sees the client, not the edge location. With origin.forward_client_ip, gp-client-ip carries the address the first edge location saw, never a GraphPilot address. See headers.
  • Your origin is called from the shield. Keep the whole GraphPilot address set in your origin allowlist; requests that do not go through the shield, and fallbacks, still come from other locations.

A shielded request is one request in Insights, with the hop recorded on it:

  • The request detail panel shows a Shield set on its Overview tab: the shield location with its city and country, how the hop ended (Result) and, when the shield called your origin, that call’s status, latency and retries; the Diagnostics set names the shield server that answered. On the Cache tab, a request the shield answered without storing shows the shield’s own reason, marked Decided at the shield. When the shield called your origin, the GraphQL tab shows the document your origin received as the shield recorded it; when the shield answered from its cache, the tab says nothing was sent to the origin.
  • The list can be filtered by Shield result and show it as a column, on every plan. A request the shield sent to your origin (MISS or PASS) is marked when GraphPilot changed its document, and the Rewritten by GraphPilot filter finds it. Shielded requests recorded before runtime 1.6.0 stay unmarked.
  • GraphQL errors your origin answered the shield with are recorded on the request, as on an unshielded one: the GraphQL errors column and preset, the error groups and the alert rule GraphQL errors in your responses count them, including errors removed before the client saw them. A request the shield answered from its cache (HIT or STALE) records none. Shielded requests recorded by runtime 1.8.0 or earlier carry no GraphQL errors.
  • Origin outcome reads Called via shield when the shield called your origin, Not called (shield cache) when it answered from its cache, and Shield failed when it gave no usable answer, so a shielded request is never counted as served without your origin when it was not.
  • The Cache hit card has a Shield section on a service that had shielded traffic in the time range: Hit (the share of shielded requests the shield answered from its cache, fresh or stale), Offload (the share of all requests the shield answered, so your origin never saw them), Fallback (the share of shielded requests where the shield could not be used: FALLBACK, TIMEOUT or UNAVAILABLE) and Shielded (how many requests went to the shield). A click on Hit or Fallback filters the list to those requests. A falling Hit with a low Fallback means the shield is reachable but misses; a rising Fallback means the shield is not being used.

In the log fields, shield_pop names the shield location and shield_result how the hop ended (HIT, MISS, STALE, PASS, FALLBACK, TIMEOUT or UNAVAILABLE). shield_node names the server within the shield that answered, so it is empty when no answer came from the shield (FALLBACK, TIMEOUT, UNAVAILABLE). The fields are empty on a request that did not go to the shield. See log fields.

Usage is counted in request units, the way the CDN bills: per request at each edge location that ran it, by the size of the body that location produced. The shield is an edge location too, so a request sent through the shield usually uses twice the request units of the same request without a shield:

  • A 2 KB response through the shield is 2 request units, a 25 KB one 6 (two locations, three started 10 KiB each).
  • A request the shield ran counts at both locations, whatever it answered, and also when the hop timed out.
  • A request that fell back before the shield ran it counts at the edge location only.
  • A background refresh through the shield is a request of its own, counted at the shield.
  • A hit at the edge location, and any request that does not go through the shield, counts at one location as before.

Insights still shows a shielded request as one request, so the request units for a period are higher than the request count Insights shows. The full table is on quotas and lean mode.

An entry is also stored at the shield location, whichever location requested it. A shield in the EU keeps that shared copy in the EU; the copies at the requesting locations stay where they are. Take the shield location into account in a data protection review. See where data lives.