Preset

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.

presetp50p95p99errorRate
stripe 200ms 800ms 2s 0.2%
Payment UX requires testing at p99

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.

Quick start

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 };
}
Wrapping a client

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;
}
Testing

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
});
Individual wrapping

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'
});
Production safety

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');
Idempotency keys are critical

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.

What to test

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?
TypeScript

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.