Optimizing your subscriptions
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.
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.
Anchor to RequirementsRequirements
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
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 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.
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:
| 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.
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, usequery_variablesandfields_changedto identify the deleted resource. A query for the deleted node might returnnull.
Group triggers only when they represent the same action, changed entity, deepest ID, and compatible query root.
Anchor to Before optimizationBefore optimization
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
Anchor to After optimizationAfter 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
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
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:
- Check every trigger against the Events reference for the API version in
[events]. - Validate every GraphQL operation against the same GraphQL Admin API version.
- Confirm that each trigger provides all variables required by its query.
- Confirm that an optimized subscription covers every field from the original query, or mark the field as no longer required.
- Confirm that the app has the access scopes required by every query field.
- Deploy the configuration and inspect real deliveries, including
data,errors,fields_changed, andquery_variables. - 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.
Anchor to Next stepsNext steps
- Filter Events deliveries: Narrow
updatedeliveries with triggers and gate deliveries with query filters. - Events delivery structure: Understand query variables, custom queries, payload limits, and delivery metadata.
- Troubleshoot Events: Inspect delivery logs and diagnose failures.