---
title: Optimizing your subscriptions
description: >-
  Split Events subscriptions by action and changed entity to reduce delivery
  volume, GraphQL work, and payload size without losing data coverage.
source_url:
  html: 'https://shopify.dev/docs/apps/build/events/optimizing-your-subscriptions'
  md: 'https://shopify.dev/docs/apps/build/events/optimizing-your-subscriptions.md'
api_name: events
---

# Optimizing your subscriptions

**Developer preview:**

Events is in developer preview on the [`unstable`](https://shopify.dev/docs/api/usage/versioning#making-requests-to-an-api-version) API version, available today for a [subset of topics](https://shopify.dev/docs/api/events). Use it for early testing ahead of a stable release and broader topic coverage. For topics not yet supported, use webhooks alongside Events in the same `shopify.app.toml`. As Events expands topic coverage, it will become the primary subscription mechanism.

Broad Events subscriptions can deliver changes your app doesn't use and repeatedly query large connections. You can reduce that work by mapping each required data point to an exact trigger, using the deepest available ID, and querying the changed node directly.

This guide shows how to optimize existing Events subscriptions while preserving the data your app needs.

***

## Requirements

Before you optimize a subscription:

* Create a working [Events subscription](https://shopify.dev/docs/apps/build/events/get-started) in `shopify.app.toml`.
* List every field that your app reads from the delivery, including fields used by `query_filter`.
* Identify whether your app needs an atomic snapshot or can process changes incrementally.
* Use the [Events reference](https://shopify.dev/docs/api/events) for the API version configured in `[events]` to verify supported triggers and available query variables.

***

## Starting subscription

The following subscription fires for every product update and reloads up to 100 variants, even when only a product-level field changed:

## shopify.app.toml

```toml
[events]
api_version = "unstable"


[[events.subscription]]
handle = "product-updated"
topic = "Product"
actions = ["update"]
uri = "https://your-app.example.com/events/products"


query = """
  query broad_product_update($productId: ID!) {
    product(id: $productId) {
      id
      title
      status
      variants(first: 100) {
        nodes {
          id
          price
          barcode
        }
      }
    }
  }
"""
```

***

## Step 1: Inventory the required data

Treat every field selected by the existing `query` as required unless you know that your app doesn't use it. Include relationship IDs that let your app join data delivered by different subscriptions.

Create a coverage table that maps each required data point to the change that should update it:

| Required data point | Change trigger | Deepest available ID | Query root | Join key |
| - | - | - | - | - |
| Product title | `product.title` | `productId` | `product(id:)` | `productId` |
| Product status | `product.status` | `productId` | `product(id:)` | `productId` |
| Variant price | `product.variants.price` | `variantsId` | `productVariant(id:)` | `productId` |
| Variant barcode | `product.variants.barcode` | `variantsId` | `productVariant(id:)` | `productId` |

A field has targeted coverage only when a supported trigger delivers the change and a valid query returns the required data. Don't remove a broad subscription until every required data point has targeted coverage.

***

## Step 2: Check trigger and variable compatibility

The variables available to an Events `query` depend on the action and trigger that caused the delivery. Every required query variable must be available for every action and trigger in the subscription.

For example, the `Product` topic exposes the following variables for these triggers:

| Trigger | Available variables |
| - | - |
| `product.title` | `productId` |
| `product.status` | `productId` |
| `product.variants.price` | `productId`, `variantsId` |
| `product.variants.barcode` | `productId`, `variantsId` |

A query that requires `variantsId` can't share a subscription with `product.title`, because a title change doesn't provide `variantsId`. Split the triggers into separate subscriptions instead.

When a subscription has multiple triggers, use only variables available to all of them. A triggerless `update` subscription can fire for changes at different depths, so don't require a child ID unless the [Events reference](https://shopify.dev/docs/api/events) confirms that every case provides it.

Parent-style triggers, such as `product.variants.*`, include their supported descendants. They don't mean only that membership in the connection changed. Check for overlapping trigger prefixes before you create separate subscriptions that might deliver the same change.

***

## Step 3: Split by action and changed entity

Use a separate subscription when actions or changed entities provide different variables or need different query roots:

* For `create`, query the new topic resource by its ID.
* For `update`, use exact triggers and the deepest ID available for the changed entity.
* For `delete`, use `query_variables` and `fields_changed` to identify the deleted resource. A query for the deleted node might return `null`.

Group triggers only when they represent the same action, changed entity, deepest ID, and compatible query root.

### Before optimization

The following subscription fires for every product update and reloads up to 100 variants, even when only a product-level field changed:

## shopify.app.toml

```toml
[events]
api_version = "unstable"


[[events.subscription]]
handle = "product-updated"
topic = "Product"
actions = ["update"]
uri = "https://your-app.example.com/events/products"


query = """
  query broad_product_update($productId: ID!) {
    product(id: $productId) {
      id
      title
      status
      variants(first: 100) {
        nodes {
          id
          price
          barcode
        }
      }
    }
  }
"""
```

### After optimization

Split product-owned fields from variant-owned fields. The variant subscription queries the changed variant directly and includes `product.id` so the handler can associate the variant with its product:

## shopify.app.toml

```toml
[events]
api_version = "unstable"


[[events.subscription]]
handle = "product-details-updated"
topic = "Product"
actions = ["update"]
triggers = [
  "product.title",
  "product.status"
]
uri = "https://your-app.example.com/events/products"


query = """
  query product_details_update($productId: ID!) {
    product(id: $productId) {
      id
      title
      status
    }
  }
"""


[[events.subscription]]
handle = "product-variant-details-updated"
topic = "Product"
actions = ["update"]
triggers = [
  "product.variants.price",
  "product.variants.barcode"
]
uri = "https://your-app.example.com/events/products"


query = """
  query product_variant_update($variantsId: ID!) {
    productVariant(id: $variantsId) {
      id
      price
      barcode
      product {
        id
      }
    }
  }
"""
```

The optimized configuration can create more deliveries when one operation changes both product and variant data. However, each delivery has a focused payload and avoids reloading unchanged variants. Use the unique `handle` values to route each payload to the appropriate processing logic.

***

## Step 4: Replace broad connections carefully

Connections such as `variants(first:)`, `media(first:)`, and `metafields(first:)` can return unchanged siblings and still omit the changed node when it falls outside the requested page. Move selected child fields to a targeted node query when the trigger provides the child's ID.

If the Events reference doesn't provide the child ID that you need, then use one of these approaches:

* Query the smallest necessary parent connection, include `pageInfo`, and reconcile truncated results.
* Maintain a local parent-to-child ID index and join using the injected parent ID.
* Subscribe to a supported relationship change that provides both IDs.

An arbitrary `first: N` limit is partial coverage. If your app requires a complete collection, then page through it in a separate reconciliation process instead of treating a capped delivery query as a full snapshot.

***

## Step 5: Target metafield changes

If an existing query selects a metafield connection, then inventory the namespaces and keys that your app uses before replacing it. Use a namespace-only trigger when every key in a namespace must stay current, or a namespace-and-key trigger when only a specific key is required.

The following subscription receives changes for every metafield key in the `custom` namespace and queries only the changed metafield:

## shopify.app.toml

```toml
[[events.subscription]]
handle = "product-custom-metafield-updated"
topic = "Product"
actions = ["update"]
triggers = [
  "product.metafield(namespace: 'custom').value"
]
uri = "https://your-app.example.com/events/products"


query = """
  query product_metafield_update(
    $productId: ID!
    $metafieldNamespace: String!
    $metafieldKey: String!
  ) {
    product(id: $productId) {
      id
      metafield(namespace: $metafieldNamespace, key: $metafieldKey) {
        id
        namespace
        key
        type
        value
      }
    }
  }
"""
```

A namespace-only trigger includes key-specific changes in that namespace. Don't add overlapping namespace-and-key triggers unless the subscriptions intentionally use different payloads or destinations.

***

## Step 6: Use query filters only for eligibility

Use `triggers` to detect what changed and `query_filter` to decide whether the current data meets an independent business condition. Keep every field referenced by `query_filter` in the same subscription's `query`.

Don't use a filter that hides a transition your app must process. For example, if your app maintains the set of active products, a filter that sends deliveries only when `product.status:'ACTIVE'` suppresses the delivery when a product becomes inactive. Without that delivery, your app can't remove the product from its active set.

Test each filter clause with matching and non-matching changes, including both directions of state transitions such as active to inactive and inactive to active.

***

## Step 7: Add reconciliation

Targeted subscriptions keep individual stored nodes current, but delivery processing can fail or arrive out of order. Use a separate reconciliation process with targeted GraphQL Admin API queries or [bulk operations](https://shopify.dev/docs/api/usage/bulk-operations/queries) to repair missed work and page through complete collections.

After Shopify removes a relationship or deletes a resource, the query result might be `null`. Use `query_variables` and `fields_changed` to remove the local relationship or resource instead of depending only on `data`.

If concurrent deliveries update the same record, then use a source timestamp such as `updatedAt` when appropriate so that your app keeps the newest state.

***

## Step 8: Validate the optimized subscriptions

Before you deploy the optimized configuration:

1. Check every trigger against the [Events reference](https://shopify.dev/docs/api/events) for the API version in `[events]`.
2. Validate every GraphQL operation against the same GraphQL Admin API version.
3. Confirm that each trigger provides all variables required by its query.
4. Confirm that an optimized subscription covers every field from the original query, or mark the field as no longer required.
5. Confirm that the app has the access scopes required by every query field.
6. Deploy the configuration and inspect real deliveries, including `data`, `errors`, `fields_changed`, and `query_variables`.
7. Test create, update, delete, relationship removal, and both directions of filtered state transitions that apply to your app.

Record changes to handles, payload roots, delivery count, and cross-subscription ordering so that you can update handler routing and monitoring. Don't claim exact savings until you measure delivery volume, GraphQL work, and payload size in your app.

***

## Next steps

* [Filter Events deliveries](https://shopify.dev/docs/apps/build/events/delivery-filtering): Narrow `update` deliveries with triggers and gate deliveries with query filters.
* [Events delivery structure](https://shopify.dev/docs/apps/build/events/delivery-structure): Understand query variables, custom queries, payload limits, and delivery metadata.
* [Troubleshoot Events](https://shopify.dev/docs/apps/build/events/troubleshoot): Inspect delivery logs and diagnose failures.

***
