TryGhost/Ghost · error · Error
Timed out waiting for Stripe sync for tier ${tier.name}
Error message
Timed out waiting for Stripe sync for tier ${tier.name} What it means
Thrown by the tiers sync-wait loop after polling for Stripe price sync and never observing the expected count (2 prices per product). Each iteration counts prices whose `product` matches the tier's Stripe product id; if it never reaches 2 within the configured deadline, the loop exhausts and throws.
Source
Thrown at e2e/helpers/playwright/flows/tiers.ts:46
const intervalMs = 250;
const startTime = Date.now();
while ((Date.now() - startTime) < timeoutMs) {
const product = opts.stripe.getProducts().find(item => item.name === tier.name);
const syncedPrices = product
? opts.stripe.getPrices().filter(item => item.product === product.id).length
: 0;
if (syncedPrices === 2) {
return tier;
}
await new Promise<void>((resolve) => {
setTimeout(resolve, intervalMs);
});
}
throw new Error(`Timed out waiting for Stripe sync for tier ${tier.name}`);
}
View on GitHub (pinned to 47d8b0e2ad)
Solutions
- Confirm Stripe Connect is set up in the test environment before creating tiers.
- Verify the tier is configured with both monthly and yearly pricing (two prices expected).
- Check the Stripe test service webhook client is delivering product/price events.
- If the environment is slow, increase the polling timeout/interval rather than letting it exhaust.
Example fix
// before
await new Promise<void>((resolve) => { setTimeout(resolve, intervalMs); });
// ... after loop ...
throw new Error(`Timed out waiting for Stripe sync for tier ${tier.name}`);
// after — extend the deadline and surface progress
const deadline = Date.now() + maxWaitMs;
while (Date.now() < deadline) {
const synced = stripe.getPrices().filter(p => p.product === product.id).length;
if (synced === 2) return tier;
await new Promise(r => setTimeout(r, intervalMs));
}
throw new Error(`Timed out waiting for Stripe sync for tier ${tier.name} (found ${stripe.getPrices().filter(p => p.product === product.id).length} prices)`)"; Defensive patterns
Strategy: retry
Type guard
function tierHasTwoPrices(tier: {stripe_product_id?: string}, stripe: {getPrices(): {product: string}[]}): boolean {
const count = stripe.getPrices().filter(p => p.product === tier.stripe_product_id).length;
return count === 2;
} Prevention
- Set up Stripe Connect before creating tiers.
- Configure both monthly and yearly prices on the tier.
- Confirm the webhook client delivers product/price events.
- Size the polling timeout to the environment's typical sync latency.
When it happens
Trigger: A tier is created/edited and the test waits for Stripe to sync its monthly + yearly prices, but the Stripe product never accumulates two prices within the polling window. Caused by Stripe webhook not delivering price updates, or the product/prices not being created in the test Stripe instance.
Common situations: Stripe test service is disconnected; webhook for `price.created` not delivered or processed; the tier was saved without a valid Stripe connection; polling interval/timeout too short for a slow environment; only one cadence was configured on the tier.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Expected redeemed offer member ${emailAddress} to have a sub
- Invalid donation amount: ${value}
- No staff invitation email found for ${email}
- Latest Stripe checkout session does not include a success UR
- Latest Stripe checkout session does not include a success UR
AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13).
Data as JSON: /api/errors/fcb7cb3d91899298.
Report an issue: GitHub.