The API timed out because the email job ran in the request
Users hit submit on onboarding. The Nest handler sent email, wrote audit rows, and called a webhook before responding.
Under load the p95 climbed. Next.js looked fine. The API was doing too much synchronously.
Web up front, API behind
We ran a full-stack product with Next.js for web and NestJS for APIs. SSR handled discovery and first paint. The API owned contracts and side effects.
The split was right. The boundaries inside the API were not.
Email ran inside the handler
Slow and retry-prone work lived in controllers. Users waited on jobs that could fail independently of their action.
One timeout produced duplicate retries upstream.
Fast answer, async work
Answer fast on the request path. Push notifications, imports, and webhook fan-out to a queue.
Make the handoff explicit in logs so ops can trace request ID to job ID.
Controller to queue handoff
@Post('signup')
async signup(@Body() dto: SignupDto) {
const user = await this.users.create(dto);
await this.queue.enqueue('send-welcome', { userId: user.id });
return { id: user.id, status: 'pending_email' };
}Split the request path
Visible boundaries matter more than labels. Answer fast for users. Be honest about async work.