Every SaaS cancel flow has the same problem: you ask "are you sure?" and then the customer is gone.
Some of those customers do not actually want to leave. They are going through a slow quarter, taking parental leave, or just stretched thin that month. Given the choice between canceling or staying active, many would choose a third option: pause.
A subscription pause lets a customer stop billing temporarily without losing their account, data, or seat. When their situation changes, they resume with one click. For some SaaS products, adding a pause option reduces voluntary cancellations by 20-30% -- without a discount, a concession, or a support ticket.
This post covers when to offer pause, the decisions you need to make upfront, and how to implement it with Stripe and Drizzle ORM in a Next.js SaaS boilerplate.
A pause works when your product has a reason to come back. Seasonal businesses, consultants between projects, and companies going through a budget freeze all have a genuine intent to return. Offering pause to these users keeps the relationship alive.
It does not work as a retention tool if your product is "sticky" only because of the data stored inside it. Users who do not find value will pause and never return -- which costs you nothing more than a delayed cancellation, but sets an expectation that pause is always available.
The rule: offer pause when you genuinely believe the user will come back. Gate it inside your cancel flow so it appears as a save offer after the user initiates cancel, not as a default option in settings.
Stripe does not have a native pause endpoint. You handle it yourself, which means you need to decide:
Decide these before you write a line of code. They affect the UI, the database schema, and the background job that expires stale pauses.
Since Stripe lacks a pause endpoint, you handle it at the subscription level:
cancel_at_period_end: true. This stops future billing without terminating the current access period.paused_until column on the subscription row, plus a status field set to paused.status: active, and clear paused_until.customer.subscription.deleted Stripe webhook) sets status to canceled when paused_until has passed without a resume.Here is the database shape using Drizzle ORM:
// modules/subscription/subscription.schema.ts
import { pgTable, uuid, text, timestamp } from 'drizzle-orm/pg-core'
export const subscriptionTable = pgTable('subscriptions', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull(),
stripeSubscriptionId: text('stripe_subscription_id'),
// active | paused | canceled
status: text('status').notNull().default('active'),
pausedUntil: timestamp('paused_until'),
currentPeriodEnd: timestamp('current_period_end'),
})
And the service method that initiates a pause:
// modules/subscription/subscription.service.ts
async function pauseSubscription(userId: string, pauseDays: number) {
const sub = await subscriptionRepo.findByUserId(userId)
if (!sub || sub.status !== 'active') throw new HttpError(400, 'No active subscription')
if (pauseDays > 90) throw new HttpError(400, 'Maximum pause is 90 days')
await stripe.subscriptions.update(sub.stripeSubscriptionId, {
cancel_at_period_end: true,
})
const pausedUntil = new Date()
pausedUntil.setDate(pausedUntil.getDate() + pauseDays)
await subscriptionRepo.update(sub.id, { status: 'paused', pausedUntil })
}
The key insight: cancel_at_period_end: true is reversible. You can undo it by updating the subscription again when the user resumes, which gives you time to collect payment before actually ending access.
The pause offer works best as a save step -- shown after the user clicks "Cancel subscription" and before the cancel is confirmed. A practical flow:
This sequencing shows the pause only to users who might genuinely benefit, not as a default that trains users to pause repeatedly instead of engaging with the product.
Make resuming frictionless. When a paused-account user logs in, show a top-of-page notice: "Your plan is paused until [date]. Resume now to restore access."
The resume action creates a new Stripe subscription. If the user's card has expired since they paused, this is where you catch it and prompt for an updated payment method. The Stripe subscriptions guide covers the checkout session pattern you can reuse for the resume flow.
A user who pauses instead of cancels keeps your monthly revenue whole during the pause window -- and more importantly, they are far more likely to reactivate than someone who fully canceled. You do not spend a marketing dollar on re-acquisition because the relationship never ended.
The implementation builds on what is already in a Next.js SaaS boilerplate: Stripe is wired, Drizzle ORM is set up, and Resend can send the pause confirmation and the upcoming-expiry reminder. Pause is an extension of the infrastructure you already have, not a separate system to maintain.
If you are building on this boilerplate, add the pause option to your next cancel flow update -- it is one of the highest-ROI retention features you can ship in an afternoon.