Skip to content

Stripe payment retry: a missing subscription (resource_missing) is retried forever #1420

Open

Description

@saurab018

Summary

When Stripe answers a retry with resource_missing — the subscription no longer exists, or was created under the other API mode — the retry path logs the error and returns. Nothing marks the row as unrecoverable, so the daily payment-retry cron asks Stripe for the same impossible object again the next day, and every day after that, forever.

Evidence from a live customer site

Found while investigating an unrelated support ticket. urm-pg-stripe-*.log on that site (URM Pro 6.2.7, live keys):

1596 x  ERROR  Stripe subscription retry failed
  13 x  ERROR  Stripe subscription ID not found for retry
  10 x  ERROR  Subscription cannot be recovered.

Every one of the 1596 is the same row:

{
    "error_code": "resource_missing",
    "error_message": "No such subscription: 'sub_1TtKb3HfxGcS6BDDLKVdGcIl'; a similar object exists in test mode, but a live mode key was used to make this request.",
    "subscription_id": "65",
    "user_id": "unknown"
}

Local subscription #65 is holding a test-mode Stripe subscription ID, almost certainly left behind when the site was tested with test keys before going live. The site runs live keys now, so the lookup can never succeed. First occurrence 4 July, peak ~490/day on 6–7 July, still firing once a day on 24 August — 51 days of guaranteed-useless API calls.

The log is 1.45 MB, most of it this one loop.

Root cause

modules/membership/includes/Admin/Services/Stripe/StripeService.php, in retry_subscription():

} catch ( ApiErrorException $e ) {
    PaymentGatewayLogging::log_error( 'stripe', 'Stripe subscription retry failed' . ... );
    $response['message'] = $e->getMessage();
    return $response;
}

A permanent failure (resource_missing) is handled identically to a transient one (network blip, rate limit). There is no give-up state, and the cron's only bound is a date window in MembersSubscriptionRepository::get_subscriptions_to_retry(), not the failure kind.

Why this needs attention now

PR #1398 (sync of pro #1396) changes failed_payment_retry_callback() to stop discarding the gateway's return value: on failure it now records an order note and emails the member.

Today this loop is invisible — it only writes to a log file. Once that lands, the same dead subscription ID produces a failed-order note and a customer-facing email every single day. On the site above that would have been ~1596 order rows and ~1596 emails to one member.

Proposed fix

Treat resource_missing as terminal and stop retrying that gateway subscription:

  • In the catch, when $e->getStripeCode() === 'resource_missing', record a marker keyed by the gateway subscription ID.
  • At the top of retry_subscription(), next to the existing empty-sub_id guard, return early when the marker is present.

Keying the marker by the gateway ID rather than the local row ID makes it self-invalidating: if an admin corrects the subscription ID on the row, the new ID is not blocked and retries resume. No cleanup code required.

The guard belongs in retry_subscription() because every retry caller routes through it.

Out of scope, filed separately

  • user_registration_payment_retry_count is not an attempt cap. It is only multiplied into a date window (retry_count * retry_interval days) in get_subscriptions_to_retry(); urm_is_payment_retrying is incremented but never compared against the configured maximum. So "retry 3 times, every 3 days" behaves as "retry daily for 9 days."
  • PayPal has no equivalent give-up path in NewPaypalService::retry_subscription(). Not seen looping in the wild yet, and the marking needs a gateway-specific error code.

Activity

  1. added a commit that references this issue on Sep 10, 2026
    c873eb2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions