Skip to main content

Optimizing your subscriptions

Developer preview

Events is in developer preview on the unstable API version, available today for a subset of topics. 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.


Before you optimize a subscription:

  • Create a working Events subscription 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 for the API version configured in [events] to verify supported triggers and available query variables.

Anchor to Starting subscriptionStarting subscription

The following subscription uses product.* to receive all supported product updates and reloads up to 100 variants, even when only a product-level field changed:

shopify.app.toml

[events]
api_version = "unstable"

[[events.subscription]]
handle = "product-updated"
topic = "Product"
actions = ["update"]
triggers = ["product.*"]
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
}
}
}
}
"""

Anchor to Step 1: Inventory the required dataStep 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 pointChange triggerDeepest available IDQuery rootJoin key
Product titleproduct.titleproductIdproduct(id:)productId
Product statusproduct.statusproductIdproduct(id:)productId
Variant priceproduct.variants.pricevariantsIdproductVariant(id:)productId
Variant barcodeproduct.variants.barcodevariantsIdproductVariant(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.


Anchor to Step 2: Check trigger and variable compatibilityStep 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:

TriggerAvailable variables
product.titleproductId
product.statusproductId
product.variants.priceproductId, variantsId
product.variants.barcodeproductId, 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. An update subscription requires triggers. A broad trigger such as product.* can fire for changes at different depths, so don't require a child ID unless the Events reference 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.


Anchor to Step 3: Split by action and changed entityStep 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.

The following subscription uses product.* to receive all supported product updates and reloads up to 100 variants, even when only a product-level field changed:

shopify.app.toml

[events]
api_version = "unstable"

[[events.subscription]]
handle = "product-updated"
topic = "Product"
actions = ["update"]
triggers = ["product.*"]
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
}
}
}
}
"""

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

[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.


Anchor to Step 4: Replace broad connections carefullyStep 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.


Anchor to Step 5: Target metafield changesStep 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

[[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.


Anchor to Step 6: Use query filters only for eligibilityStep 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.


Anchor to Step 7: Add reconciliationStep 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 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.


Anchor to Step 8: Validate the optimized subscriptionsStep 8: Validate the optimized subscriptions

Before you deploy the optimized configuration:

  1. Check every trigger against the Events reference 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.



Was this page helpful?