Senior Application Developer

loc: Maynooth, ON K0L 2S0tel: 613-553-0960email: rgsamways@gmail.com
Robin Samways

Farpost · Payments

$ The Fix That Almost Reopened the Bug It Fixed

Caching a customer's Stripe ID fixed a real race: Stripe's customer-search API is eventually consistent, so searching for "does this customer already exist" right after creating them could miss it and create a duplicate. But caching introduced a new, quieter bug — if that customer is later deleted directly in the Stripe dashboard, the cached ID points at nothing, and invoicing breaks with a "No such customer" error.

The fix isn't to stop caching — that would reopen the original race. It's to verify the cached ID before trusting it, using a lookup method that doesn't share the same consistency problem the cache was built to avoid.

async def resolve_requestor_customer_id(
    cached_customer_id: str | None, email: str, name: str, metadata: dict,
) -> str:
    """Return a valid Stripe customer ID for a requestor, verifying a cached ID still
    resolves before trusting it. A cached ID can go stale if the customer is deleted
    directly in Stripe. customers.retrieve() is an exact, instant-consistent lookup by
    ID, so verifying here doesn't reintroduce the eventually-consistent-search race
    the original caching fix was protecting against."""
    client = _client()
    if cached_customer_id:
        try:
            client.customers.retrieve(cached_customer_id)
            return cached_customer_id
        except stripe.error.InvalidRequestError:
            logger.warning('{"event": "stripe_customer_id_stale", "customer_id": "%s"}',
                            cached_customer_id)
    return await get_or_create_requestor_customer(email=email, name=name, metadata=metadata)

The fix

customers.retrieve() looks a customer up by exact ID, which Stripe guarantees is instantly consistent — unlike the search API. Verifying a cached ID with retrieve() before using it catches a stale, deleted ID without reintroducing the eventual-consistency race the cache was originally built to solve. A retrieve() failure falls back to the same get_or_create path used when there was no cached ID at all.

Why this matters

This is a fix within a fix — it required actually understanding why the original caching fix worked (an exact-ID lookup being consistent when a search isn't) well enough to add a new safety check without silently undoing that reasoning. That's root-cause diagnosis applied a second time, to a problem the first fix itself created.

Feedback on this page