> ## Documentation Index
> Fetch the complete documentation index at: https://cosmo-docs.wundergraph.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Persisted Operations

> Persisted operations, also known as trusted documents, allow you to register trusted operations in the router

**Persisted operations** allow you to register queries / mutations / subscriptions within a federated graph, enabling the clients to send just an identifier in their request instead of sending the whole operation body. These operations need to be registered ahead of time, using the `wgc operations push` command.

This not only saves bandwidth but can also help reduce the attack surface by allowing only safe-listed operations.

### Architecture

<Frame caption="Workflow of `wgc` operations push and pull via CDN">
  <img src="https://mintcdn.com/wundergraphinc/ZnEwMjVtesOG4lpT/images/router/persisted-queries/workflow-of-wgc-ops-push-and-pull-via-cdn.png?fit=max&auto=format&n=ZnEwMjVtesOG4lpT&q=85&s=d2a116f3bf1aff0e4986835f9ce2b017" alt="Workflow of pushing WGC operations to CDN and pulling them back to routers" width="1720" height="1680" data-path="images/router/persisted-queries/workflow-of-wgc-ops-push-and-pull-via-cdn.png" />
</Frame>

Persisted operations are usually registered within Cosmo during your release pipeline, which pushes them to the control plane. This allows you to use GraphQL queries without previously registering them in development while allowing only trusted operations in production.

The control plane replicates these operations in the Cosmo CDN, where the routers can fetch them. All operations in the CDN are protected and only readable by routers running the federated graph to which the operations were registered.

## Using persisted operations

Persisted operations require some tooling on the client side. Consult the documentation for your GraphQL client library to find out how to generate a query manifest or query map.

### Supported manifest formats

`wgc operations push` automatically detects the format of your manifest file. The following formats are supported:

<Tabs>
  <Tab title="Apollo">
    The Apollo persisted query manifest format:

    ```json manifest.json theme={"system"}
    {
      "format": "apollo-persisted-query-manifest",
      "version": 1,
      "operations": [
        {
          "id": "dc67510fb4289672bea757e862d6b00e83db5d3c",
          "name": "GetEmployees",
          "type": "query",
          "body": "query GetEmployees { employees { id } }"
        }
      ]
    }
    ```
  </Tab>

  <Tab title="Relay">
    Relay query maps are supported in two formats — as an array of `[id, query]` pairs or as a `{id: query}` object:

    ```json relay-query-map.json theme={"system"}
    {
      "dc67510fb4289672bea757e862d6b00e83db5d3c": "query GetEmployees { employees { id } }",
      "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2": "mutation UpdateEmployee($id: ID!) { updateEmployee(id: $id) { id } }"
    }
    ```

    ```bash theme={"system"}
    wgc operations push mygraph -n default -c web -f relay-query-map.json
    ```
  </Tab>

  <Tab title="GraphQL">
    Plain `.graphql` or `.gql` files containing a single operation. The SHA-256 hash is computed automatically:

    ```graphql operations.graphql theme={"system"}
    query GetEmployees {
      employees {
        id
      }
    }
    ```

    ```bash theme={"system"}
    wgc operations push mygraph -n default -c web -f operations.graphql
    ```
  </Tab>
</Tabs>

### Pushing operations

Once your manifest is generated, push it using `wgc`:

```bash theme={"system"}
wgc operations push mygraph -n default -c web -f my-operations-manifest.json
```

This registers the operations for your federated graph named `mygraph` in the `default` namespace and your client named `web` (indicated by the `graphql-client-name` HTTP header). You can push multiple files at once using the `-f` flag multiple times.

When pushing the operations, you will see a short summary indicating how many were created and how many were already registered. Use `--format json` for machine-readable output:

```bash theme={"system"}
wgc operations push mygraph -n default -c my-client -f manifest.json --format json
```

To see all available options for `wgc operations push`, see [Push](/cli/operations/push).

Additionally, check the [Using Persisted Operation with Federated GraphQL](/tutorial/using-persisted-operations) tutorial for a step-by-step guide.

## Deleting persisted operations in Studio

You can delete persisted operations directly in Cosmo Studio after they have been uploaded via `wgc operations push`.

1. Open your federated graph in Studio.
2. Go to **Clients**.
3. Open a client and click **View Operations**.
4. Expand the operation and click the delete button.
5. Confirm in the delete dialog.

Only organization users with `organization-admin` or `organization-developer` roles can delete operations.
Operations are currently deleted one at a time from the UI.

<Note>
  Studio always asks for confirmation before deleting operations. If traffic is detected for the selected operation, the dialog warns that the operation is receiving traffic. If analytics data is unavailable, Studio cannot guarantee that existing clients won't break. You can always check the metrics using the link in the dialog.
</Note>

## PQL Manifest

By default, the router fetches persisted operations individually from the Cosmo CDN on each request. When the **PQL manifest** is enabled, the router instead loads all persisted operations from a single manifest file at startup and serves them entirely from memory, eliminating per-request network overhead.

```yaml theme={"system"}
persisted_operations:
  manifest:
    enabled: true
```

The manifest is automatically updated in the Cosmo CDN whenever operations are added or deleted via `wgc operations push` or Studio. The router polls for updates using `poll_interval` and `poll_jitter`, picking up changes without requiring a restart. You can also load the manifest from a custom [storage provider](#using-a-custom-storage-provider).

<Note>
  When the manifest is enabled, it is **authoritative** — the router does not fall back to fetching individual operations from the CDN. Unknown operation hashes are rejected immediately.
</Note>

### Cache warmup

When the PQL manifest is enabled, the router automatically warms up its caches by pre-processing all operations from the manifest at startup and after each manifest update. Each operation goes through parsing, normalization, validation, and query planning — the same steps that happen on a regular request. Once warmed, every operation is served entirely from cache with zero processing overhead on the first request.

Cache warmup is enabled by default and can be configured or disabled:

```yaml theme={"system"}
persisted_operations:
  manifest:
    enabled: true
    warmup:
      enabled: true        # default: true
      workers: 4            # number of concurrent workers (default: 4)
      items_per_second: 50  # rate limit, 0 = unlimited (default: 50)
      timeout: 30s          # maximum time for warmup to complete (default: 30s)
```

| Option             | Default | Description                                                                                                                                    |
| ------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`          | `true`  | Set to `false` to disable cache warmup for manifest operations.                                                                                |
| `workers`          | `4`     | Number of concurrent workers used to pre-process operations. Increase for large manifests.                                                     |
| `items_per_second` | `50`    | Maximum items processed per second across all workers. Actual throughput can be lower if processing is slow. Set to `0` for unlimited.         |
| `timeout`          | `30s`   | Maximum time allowed for the warmup to complete. If the timeout is exceeded, the router logs an error but continues serving requests normally. |

## Using a custom storage provider

You can load persisted operations from your own S3-compatible storage instead of the Cosmo CDN. First, [define a storage provider](/router/storage-providers), then reference it in your persisted operations configuration.

## Disallowing non-persisted Operations

If you're going all in on Security, you'd want to only allow Persisted Operations in your Production Environment.

By default, non-persisted (dynamic) GraphQL Operations are allowed, which you can disable using the [Security Configuration](/router/configuration#security) of the Router.

We expose 4 different types of persisted operation blocking in the configuration:

1. **Allow all operations** (Default) — Both persisted and dynamic operations are permitted.

2. **Log unknown operations** — Any operation that has not been persisted will be logged, but not blocked.

3. **Safelist** — Operations that have been explicitly persisted will be allowed, based on matching the query body against persisted queries.

4. **Block non-persisted operations** — Fully enforced blocking of non-persisted operations, clients are required to send a pre-computed SHA-256 hash instead of a query body.

### Migration path to enforcing persisted operations

To migrate from allowing all operations to a more restrictive option incrementally, we recommend following these steps:

<Steps>
  <Step title="Enable log_unknown">
    This will log when clients use operations that have not been persisted, helping you identify which operations to persist.

    <CodeGroup>
      ```bash config.yaml theme={"system"}
      persisted_operations:
        log_unknown: true
      ```
    </CodeGroup>
  </Step>

  <Step title="Enable safelist">
    This will allow users to send operations with any query body, but only execute if they match a persisted operation.

    <CodeGroup>
      ```bash config.yaml theme={"system"}
      persisted_operations:
        log_unknown: true
        safelist:
          enabled: true
      ```
    </CodeGroup>
  </Step>

  <Step title="Enable block_non_persisted_operations">
    This will block all non-persisted operations, requiring clients to send a pre-computed SHA-256 hash instead of a query body.

    <CodeGroup>
      ```bash config.yaml theme={"system"}
      security:
        block_non_persisted_operations:
          enabled: true
      ```
    </CodeGroup>
  </Step>
</Steps>

<Info>
  #### Important Considerations

  * **Whitespace sensitivity** Differences in whitespace can alter an operations hash, which will cause it to be rejected as an unknown operation. We recommend using `log_unknown_operations` before enabling full blocking.

  * **Compatibility with Automatic Persisted Queries (APQ)** The `safelist` option cannot be used alongside APQ, as their functions are opposite.
</Info>
