# Why Next.js Background Tasks Die in Production

> Unawaited promises and setTimeout freeze silently on serverless. Why your background tasks die in production and how to run real workers.

Source: https://boringpush.com/blog/why-nextjs-background-tasks-die-in-production

It works on your laptop. You test the sign-up flow on localhost.

You hit the submit button, your API route answers in fifty milliseconds, and two seconds later the welcome email lands cleanly in your inbox.

You push to production on Vercel or AWS Lambda. In production, half of your emails never arrive.

There are no crash logs, no red error messages, and no failed build notices. The background task simply vanished into thin air.

## The illusion of local development

To understand why this happens, you have to look at the difference between your local machine and a serverless production runtime.

When you run `npm run dev`, Next.js spins up a long-running Node.js process. When an API route sends a response with `Response.json()`, the HTTP connection closes, but the underlying Node.js event loop keeps turning.

If you fired an unawaited promise or a `setTimeout()`, Node finishes processing it in the background while you sip coffee.

Serverless platforms do not work that way.

In a serverless environment like Vercel or AWS Lambda, computing power is billed by the millisecond. The platform is designed to shut down resources the instant a request completes.

As soon as your route handler returns its response, the platform freezes the execution container.

## What happens when a container freezes

When a serverless container freezes, its CPU allocation drops to zero.

If an outbound network request to Resend or SendGrid was halfway through a TLS handshake, the packets stop moving. If a database query was awaiting an insert confirmation, the connection drops.

Sometimes, if that same container instance is reused for another request ten seconds later, the frozen promise might briefly thaw and finish. More often, the container is destroyed entirely to reclaim memory.

The result is random, intermittent failure. Ten percent of your background tasks succeed, while ninety percent quietly die.

This is why unawaited promises in Next.js routes are a trap.

## Why Next.js `after()` is only half a fix

Next.js introduced a native helper function called `after()` (`import { after } from 'next/server'`).

This utility allows you to register work that should execute after the HTTP response has been flushed to the browser:

```javascript
import { after } from "next/server";

export async function POST(req) {
  const data = await req.json();
  const user = await createUser(data);

  // Scheduled to run after response is sent
  after(async () => {
    await sendWelcomeEmail(user);
    await trackSignupAnalytics(user);
  });

  return Response.json({ ok: true });
}
```

This is an improvement over raw unawaited promises because it instructs the serverless platform to keep the container awake until the callback resolves.

However, `after()` still operates within strict serverless boundaries.

First, your task is still bounded by the platform maximum duration. If you are on a free or Hobby plan, the hard timeout is ten seconds. If you are on a Pro plan, it is sixty seconds.

Second, `after()` has no retry mechanism. If your email provider or database experiences a brief connection blip, the task fails and is gone forever.

Third, `after()` cannot handle delayed work. You cannot ask `after()` to send a check-in message two hours later.

## The architectural boundary: HTTP vs Queues

HTTP request handlers are built for rapid, synchronous communication. Their job is to accept input, validate authorization, write immediate state, and return a response to the user.

Any task that takes longer than two hundred milliseconds, or depends on external network stability, belongs in a queue.

When you separate request handling from task execution, three things change:

First, your user-facing endpoints become blazing fast because they never wait for third-party networks.

Second, your background tasks gain durability. If your server reboots or an upstream API returns a 503 error, the task is safely preserved in storage.

Third, you gain backoff and retry guarantees without paying cloud queue fees.

## The $0 VPS pattern: Persistent workers on SQLite

If you deploy Next.js to your own virtual private server using PM2 or Docker, the serverless execution freeze problem disappears.

Your server is a persistent machine. The event loop never freezes.

In BoringPush, this pattern is implemented through `boringqueue` on top of local SQLite:

1. The Next.js API route writes a job row to `db/queue.db` via `dispatchJob("emails/welcome", payload)`. This takes under one millisecond.
2. The route returns HTTP 200 immediately to the browser.
3. A separate worker process managed by PM2 claims the pending job from the database table.
4. If the task fails, the worker automatically retries with exponential backoff (`min(300s, 5s * attempt)`).

```javascript
// Inside your API route: instantaneous dispatch
import { dispatchJob } from "@/libs/boringqueue";

export async function POST(req) {
  const body = await req.json();
  
  // Safe on disk in 0.5ms
  dispatchJob("users/onboard", { userId: body.userId });

  return Response.json({ status: "queued" });
}
```

Because the worker runs outside the web request cycle, a task can run for five minutes without hitting a gateway timeout.

There are no containers to spin up and no third-party queue vendors to pay.

## Keep your request handlers dumb and fast

When building Next.js applications, treat the HTTP boundary with respect.

Never let a user request wait for an external API call that is not strictly required to render the next screen.

Do not trust unawaited promises in production environments. Never rely on `setTimeout` to handle background logic.

Use synchronous HTTP routes to record intent, and let persistent background workers handle the heavy lifting.

## FAQ

### Why did my unawaited promise work on Vercel sometimes?

Serverless platforms reuse warm container instances when incoming request volume is steady. If another request hits the same container shortly after yours, the frozen event loop may resume temporarily, giving the false impression that background tasks work.

### Is `next-after` safe for production analytics?

Yes, for lightweight tasks that finish in under two seconds (like firing a single telemetry ping). It is not safe for mission-critical tasks like billing webhooks, receipt delivery, or multi-step database syncs.

### Can I run background workers inside Next.js API routes using intervals?

No. Running `setInterval` inside an API route is an anti-pattern. If the route is called multiple times, you will spawn duplicate interval timers, causing memory leaks and race conditions. Background loops should always live in a dedicated worker script.

### Do I need a separate VPS to run workers?

No. On BoringPush, both your Next.js application and the worker thread pool run on the exact same $5 Hetzner or DigitalOcean VPS, managed cleanly by PM2.

### What happens if my worker crashes while running a task?

`boringqueue` uses database heartbeats. If a worker process crashes, the next heartbeat check marks the abandoned job as pending and assigns it to a healthy thread.

## Keep reading

- [Next.js Background Jobs Without Redis: $0 BullMQ](/blog/nextjs-background-jobs-no-redis-bullmq-alternative) — the full queue setup
- [Free Vercel Cron Alternative: Jobs Every Minute](/blog/free-vercel-cron-alternative-every-minute) — scheduled task alternatives
- [Ditch Vercel for a $5 VPS](/blog/ditch-vercel-5-dollar-vps) — the complete hosting migration guide
