stripe latency profile
Stripe is slower than a database but faster than an LLM. At p50 of 200ms, your checkout flow needs explicit loading states. At p99 of 2s, a user might think the payment button is broken. These are the cases slowdep helps you design for.
At p99, your checkout takes 2 seconds. Without slowdep, your local Stripe test key responds in ~50ms, and you never build the loading state, disabled button, or "Processing..." text that users actually need. A 2-second payment that looks frozen causes payment abandonment.
Wrap paymentIntents.create
The most direct approach: wrap the specific Stripe sub-resource method you use. Stripe's client uses a sub-resource pattern (stripe.paymentIntents, stripe.customers, etc.).
import Stripe from 'stripe'; import { withLatency } from 'slowdep'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); // Wrap the create method on the paymentIntents sub-resource const slowCreatePayment = withLatency( stripe.paymentIntents.create.bind(stripe.paymentIntents), 'stripe' ); // Now your checkout handler experiences realistic Stripe latency async function createCheckout(amount: number, currency: string, customerId: string) { const paymentIntent = await slowCreatePayment({ amount, // in smallest currency unit (cents for USD) currency, customer: customerId, automatic_payment_methods: { enabled: true } }); return { clientSecret: paymentIntent.client_secret }; }
Wrap an entire sub-resource with withLatencyAll
Use withLatencyAll on a specific sub-resource to wrap all its methods at once — create, retrieve, update, cancel.
import Stripe from 'stripe'; import { withLatencyAll } from 'slowdep'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); // Wrap the entire paymentIntents sub-resource const slowPaymentIntents = withLatencyAll(stripe.paymentIntents, 'stripe'); // Wrap customers too const slowCustomers = withLatencyAll(stripe.customers, 'stripe'); // Full checkout flow with realistic latency on every Stripe call async function fullCheckout(email: string, amount: number) { // Create or retrieve customer (~200ms) const customer = await slowCustomers.create({ email }); // Create payment intent (~200ms) const intent = await slowPaymentIntents.create({ amount, currency: 'usd', customer: customer.id, automatic_payment_methods: { enabled: true } }); // Confirm the intent (~200ms) — in a real flow this happens client-side const confirmed = await slowPaymentIntents.retrieve(intent.id); return confirmed; }
Test checkout UX, timeout, and webhook handling
import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { withLatency } from 'slowdep'; import { CheckoutButton } from '../src/components/CheckoutButton'; test('checkout button disables and shows "Processing..." during payment', async () => { const mockCreate = withLatency( jest.fn().mockResolvedValue({ id: 'pi_test', client_secret: 'pi_test_secret', status: 'requires_payment_method' }), { p50: 600, p99: 2000 } // realistic payment creation latency ); render(<CheckoutButton amount={4999} createFn={mockCreate} />); fireEvent.click(screen.getByRole('button', { name: /buy now/i })); // Button must disable immediately on click — no double-charging expect(screen.getByRole('button')).toBeDisabled(); expect(screen.getByText(/Processing/i)).toBeInTheDocument(); await waitFor(() => { expect(screen.getByText(/Payment successful/i)).toBeInTheDocument(); }, { timeout: 5000 }); });
import { withLatency } from 'slowdep'; import { processPayment } from '../src/payments'; test('processPayment times out after 5s and does not double-charge', async () => { const mockCreate = jest.fn().mockResolvedValue({ id: 'pi_test', status: 'processing' }); // Simulate Stripe under heavy load const verySlowCreate = withLatency(mockCreate, { p50: 6000, p99: 12000 }); await expect( processPayment({ amount: 9999, currency: 'usd' }, { create: verySlowCreate, timeoutMs: 5000 }) ).rejects.toThrow('PaymentTimeoutError'); // Verify idempotency key was used — same key means no duplicate charge expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ metadata: expect.objectContaining({ idempotencyKey: expect.any(String) }) }) ); }, 10000);
import { withLatency } from 'slowdep'; import { handleWebhook } from '../src/webhooks/stripe'; test('webhook handler responds 200 before processing (async pattern)', async () => { // Webhook handler should ack immediately, then process async // The DB write after the webhook can be slow const slowDbWrite = withLatency( jest.fn().mockResolvedValue({ id: 'order_1' }), 'postgres' ); const event = { type: 'payment_intent.succeeded', data: { object: { id: 'pi_test', amount: 4999, currency: 'usd' } } }; const start = Date.now(); const response = await handleWebhook(event, { dbWrite: slowDbWrite }); const elapsed = Date.now() - start; // Webhook must respond in <1s regardless of how slow the downstream work is expect(response.status).toBe(200); expect(elapsed).toBeLessThan(100); // ack immediately });
Wrap subscriptions and refunds
Every Stripe sub-resource follows the same pattern. Wrap only what you use — refunds may have different latency characteristics to model.
import Stripe from 'stripe'; import { withLatency } from 'slowdep'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); // Subscriptions — use stripe preset const slowCreateSub = withLatency( stripe.subscriptions.create.bind(stripe.subscriptions), 'stripe' ); // Refunds are typically processed faster const slowRefund = withLatency( stripe.refunds.create.bind(stripe.refunds), { p50: 150, p99: 1500 } ); // Checkout sessions (newer API) const slowCheckoutSession = withLatency( stripe.checkout.sessions.create.bind(stripe.checkout.sessions), 'stripe' ); const session = await slowCheckoutSession({ payment_method_types: ['card'], line_items: [{ price: 'price_1234', quantity: 1 }], mode: 'payment', success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel' });
Gate on environment — never slow real payments
import Stripe from 'stripe'; import { withLatencyAll } from 'slowdep'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); const isProd = process.env.NODE_ENV === 'production'; // These are the only Stripe sub-resources used in this app export const payments = isProd ? stripe.paymentIntents : withLatencyAll(stripe.paymentIntents, 'stripe'); export const customers = isProd ? stripe.customers : withLatencyAll(stripe.customers, 'stripe'); export const subs = isProd ? stripe.subscriptions : withLatencyAll(stripe.subscriptions, 'stripe');
When testing with realistic latency, always use idempotency keys on create calls. If your timeout fires and you retry, Stripe will return the same PaymentIntent rather than creating a duplicate charge. Your tests should verify this behavior.
Scenarios this integration helps you verify
- Button disable on click — at p50 of 200ms, users will notice a non-disabled button and may click twice. Always disable immediately on first click.
- Idempotency key handling — on timeout, your retry must use the same idempotency key to avoid double-charging
- Card decline UX — the 0.2% error rate also models card declines; verify your error state is user-friendly, not just "Error"
- Subscription creation latency — subscription creation with a trial period involves multiple internal Stripe operations; budget for 500ms+
- Webhook 200 ack before processing — Stripe retries webhooks if you don't ack within 30s; always ack immediately and process asynchronously
- Checkout session expiry — if payment is slow and the session expires (30 minutes), does your UI handle the redirect gracefully?
Typed Stripe integration
import Stripe from 'stripe'; import { withLatency } from 'slowdep'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); // Return type inferred: Promise<Stripe.PaymentIntent> const slowCreate = withLatency( stripe.paymentIntents.create.bind(stripe.paymentIntents), 'stripe' ); async function createIntent( amount: number, options?: Stripe.PaymentIntentCreateParams ): Promise<Stripe.PaymentIntent> { return slowCreate({ amount, currency: 'usd', automatic_payment_methods: { enabled: true }, ...options }); } // Fully typed — amount, currency, status all have correct types const intent: Stripe.PaymentIntent = await createIntent(4999); console.log(intent.status); // 'requires_payment_method' | 'processing' | etc.