PostHog question
Why are my posthog-node events missing when the code runs in a Vercel function?
If posthog-node events show up from a long-running local server but not from a Vercel function, they were still in the client's queue when the function returned. capture() only queues an event; the client sends a batch after flushAt events or on a timer. PostHog's docs say Vercel Functions can freeze the runtime after a handler returns, before background requests finish. Await client.flush() before returning if you reuse the client, await client.shutdown() for a client made for one invocation, or await client.captureImmediate(...) for a single event. A capture() that nothing awaits sends nothing once the process stops. Reproduced with posthog-node 5.61.1 on 2026-10-31.
What capture() does, and what a stopped process loses
PostHog documents flushAt as the number of capture calls after which the queue is sent in one batch (default 20) and flushInterval as the timer; flushInterval: 0 turns the timer off. It also says that flushAt: 1 starts a background flush after each queued event, but neither setting waits for delivery, so the function can still return first.
In a run against a local HTTP server on 2026-10-31, a process that called capture() and then exited sent nothing. The same script with an awaited flush(), shutdown() or captureImmediate() delivered one request to /batch/.
capture() then exit 0 requests received
capture() + await flush() 1 request: POST /batch/
capture() + await shutdown() 1 request: POST /batch/
await captureImmediate() 1 request: POST /batch/Should I call flush() or shutdown() in a route handler?
PostHog's Node docs say to keep one shared client outside the handler and await flush() before returning, including when your code throws. For a client created inside one invocation, await shutdown() after the last event and do not use that client again. The docs say not to call shutdown() after each request if you reuse the client.
import { PostHog } from "posthog-node"
const client = new PostHog(process.env.POSTHOG_KEY, {
host: "https://us.i.posthog.com",
})
export async function POST() {
try {
client.capture({ distinctId: "user_123", event: "signup" })
return Response.json({ ok: true })
} finally {
await client.flush()
}
}Can I send the events after the response with after() or waitUntil()?
Vercel's docs recommend after() from next/server on Next.js 15.1 or newer to schedule work that runs once the response is sent, and waitUntil() from @vercel/functions elsewhere. A promise given to waitUntil() has the same timeout as the function, and is cancelled if the function times out (see function timeouts). Putting await client.flush() inside after() fits what those docs describe, but this page did not run it on Vercel.
import { after } from "next/server"
export async function POST() {
client.capture({ distinctId: "user_123", event: "signup" })
after(() => client.flush())
return Response.json({ ok: true })
}Checklist
How to fix it, then check the fix
- 01Find every place the server code captures events:
grep -rnE "posthog-node|new PostHog\(|\.capture\(" --exclude-dir=node_modules --exclude-dir=.next . - 02Decide how long each client lives. A client at module scope is reused between invocations: await flush() before returning. A client created inside the handler is for one invocation: await shutdown() and never use it again.
- 03Put the await where it also runs when the handler throws, such as a finally block, so failed requests do not drop their events.
- 04For one important event, such as a signup or a payment, use await client.captureImmediate({ distinctId, event }) instead of queuing it.
- 05Do not rely on flushAt: 1 alone: PostHog says neither flushAt nor flushInterval waits for delivery.
- 06Redeploy, call the route once, then look for the event under the distinct ID you used in PostHog, and check that the function finishes inside its maxDuration.
Related
Related guides
Sources
Official docs behind this checklist
Browse all launch guides · Read resources · How to make and check your next change
FAQ
FAQ for builders before launch
Why does it work with next dev or a server I run myself?
Those run one long-lived process, so the queue's timer still fires seconds later. PostHog's docs say AWS Lambda or Vercel Functions can freeze the runtime after a handler returns, before background requests finish.
Should flush() have a timeout?
PostHog says an explicit flush() has no overall timeout, and that a timeout only limits the wait, because the platform can still freeze or end the runtime before delivery finishes. It says to calculate the timeout from the request deadline. Vercel's getDeadline() from @vercel/functions returns that deadline as a Date, or undefined when it is not available.
Does the same problem hit posthog-js in the browser?
This page covers posthog-node on the server. The browser SDK runs in the visitor's tab, which is not frozen when a function returns; for browser events that never show up, use the Vite and Vercel checklist.