TL;DR
-
captureException(and friends) queue the event. The HTTP POST is async. -
process.exit/os.Exit/ Lambda freeze can kill the process before that POST finishes. Dashboard stays empty. - Fix:
await Sentry.flush()orawait Sentry.close()in Node;sentry.Flush(2 * time.Second)in Go, before you die.
You ship a cron, a Lambda, a CLI, or a one-shot k8s Job. Something throws. You call captureException. The process exits. You refresh the error dashboard. Nothing. You re-check the DSN, blame "Sentry being flaky," then burn an afternoon on network paths that were fine the whole time.
The SDK did what you asked: it queued the event. The process died before the transport finished. This post is the portable flush recipe for Node (@sentry/node) and Go (getsentry/sentry-go). Same calls work against SaaS Sentry, GlitchTip, or any self-hosted ingest that speaks the Sentry protocol.
Why the last error vanishes
Capture is async. Exit ends the process. Anything still in the SDK queue never leaves the box.
Official SDKs do not block your handler on every captureException. They serialize the event, hand it to an internal transport, and return. That is correct for long-running servers: you do not want every error to stall a request.
Short-lived workers have a different lifetime. The job finishes in hundreds of milliseconds. You call process.exit(1) or the runtime freezes the Lambda. The event buffer still has work. No more event loop ticks. No more goroutine. The POST never happens.
Mental model: capture = "please send this soon." Flush/close = "block until the queue is empty (or the timeout hits), then I am allowed to exit."
Broken Node repro
Reproduce with captureException plus an immediate process.exit and no flush. Expect an empty issue stream even when the DSN is valid.
Pin the SDK you will actually run:
npm i @sentry/node@11
broken-worker.mjs:
import * as Sentry from "@sentry/node";
Sentry.init({
dsn: process.env.SENTRY_DSN, // any valid Sentry-protocol DSN
tracesSampleRate: 0,
});
async function main() {
try {
throw new Error("flush-demo: cron blew up");
} catch (err) {
Sentry.captureException(err);
// looks done… it is not
process.exit(1);
}
}
main();
Run it:
SENTRY_DSN='https://PUBLIC@HOST/PROJECT' node broken-worker.mjs
Expected when broken
- Process exits in a few ms with code
1. - Your ingest host / SaaS project shows no new issue for
flush-demo: cron blew up(or it appears much later / never, depending on race). - You will be tempted to rotate the DSN. Do not. Watch the network next.
Optional sanity check: temporarily set debug: true in Sentry.init. You often see the SDK still preparing or sending when the process already died.
Fixed Node (flush / close)
Await Sentry.flush(timeoutMs) if the process might keep running, or Sentry.close(timeoutMs) right before exit. Both return a Promise<boolean>: true if the queue drained in time.
fixed-worker.mjs:
import * as Sentry from "@sentry/node";
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 0,
});
async function main() {
try {
throw new Error("flush-demo: cron blew up");
} catch (err) {
Sentry.captureException(err);
// drain the queue, then disable the client (safe before exit)
const ok = await Sentry.close(2000);
if (!ok) {
console.warn("Sentry.close timed out; some events may be lost");
}
process.exit(1);
}
}
main();
If you only need to drain and the process continues (batch of jobs in one process), prefer flush:
const ok = await Sentry.flush(2000);
// client stays enabled
SIGTERM / graceful shutdown (Compose, k8s, systemd):
async function shutdown(signal) {
console.log(`got ${signal}, flushing Sentry`);
await Sentry.close(2000);
process.exit(0);
}
process.on("SIGTERM", () => {
shutdown("SIGTERM");
});
process.on("SIGINT", () => {
shutdown("SIGINT");
});
Pinned versions used in this post
| Package | Pin |
|---|---|
@sentry/node |
11 (tested shape against 11.0.0 docs APIs) |
Expected when fixed
-
close/flushtakes up to your timeout (here 2s) and resolvestrueon a healthy network path. - The issue for
flush-demo: cron blew upshows up on the project within a few seconds. -
lastEventId()(optional) returns an id after capture; after a successful flush you can correlate it in the UI.
AWS Lambda note: the runtime can freeze after your handler returns. Prefer flushing inside the handler before you return, not only on a process-level signal you may never see.
Go: Flush before os.Exit
Call sentry.Flush(timeout) before the program terminates. defer is fine for normal returns; os.Exit skips defers, so flush manually before Exit.
go get github.com/getsentry/sentry-go@v0.49.0
package main
import (
"fmt"
"os"
"time"
"github.com/getsentry/sentry-go"
)
func main() {
err := sentry.Init(sentry.ClientOptions{
Dsn: os.Getenv("SENTRY_DSN"),
})
if err != nil {
fmt.Fprintf(os.Stderr, "sentry.Init: %v\n", err)
os.Exit(1)
}
// covers normal return paths
defer sentry.Flush(2 * time.Second)
sentry.CaptureException(fmt.Errorf("flush-demo: job failed"))
// os.Exit skips defers; flush first
if !sentry.Flush(2 * time.Second) {
fmt.Fprintln(os.Stderr, "sentry.Flush timed out; some events may be lost")
}
os.Exit(1)
}
Expected when fixed: Flush returns true and the exception appears on the project. When broken (capture + os.Exit with no Flush), you get the same empty-dashboard trap as Node.
Do not call Flush after every CaptureException in a hot loop. That turns async transport into sync spam. Flush at boundaries: end of job, before Exit, on shutdown.
Where this bites (and where it usually does not)
You need an explicit flush on short-lived processes. Long-running servers mostly get away without it, but SIGTERM handlers still benefit.
| Runtime | Usually needs flush? | Why |
|---|---|---|
| Cron / systemd oneshot | Yes | Process exits as soon as the script ends |
| AWS Lambda / Cloud Functions | Yes | Freeze after return; buffer dies with the freeze |
CLI / go run job |
Yes | User hits Ctrl-C or you os.Exit
|
k8s Job / Compose restart: "no" one-shot |
Yes | Same as cron |
| Long-running HTTP API | Usually no | Event loop / goroutines keep running; still flush on SIGTERM |
If your "server" is actually a request-scoped worker that exits per message (some queue consumers), treat it like a cron.
Confirm steps + failure checklist
Confirm by forcing one known error, awaiting flush/close, and seeing that exact message in the project before you touch the DSN again.
Confirm
- Use a disposable message string (
flush-demo: …) so you can find it fast. - Run the fixed Node or Go snippet against a known-good DSN.
- Assert
flush/close/Flushreturnedtrue(log it). - Open the project: the issue exists with that message.
- Re-run the broken variant once so you trust the difference is flush, not luck.
Failure checklist
-
process.exit/os.Exitbefore flush. Classic. Move exit afterawait close/Flush. -
Forgot that
os.Exitskipsdefer. Go: flush explicitly before Exit; do not rely on deferred Flush alone. -
Timeout too short on a slow link.
2000ms is a starting point; bump for cold starts / far regions, or accept loss and logfalse. - Lambda returns before flush. Await flush inside the handler; do not only register process signals.
-
Wrong DSN still possible. Flush succeeding with
truebut no UI event means auth/host/project mismatch. Fix DSN after flush is proven locally (debug transport logs help). -
Sample rates /
beforeSenddropping the event. Flush only drains what the SDK kept. A filter that returnsnullwill never arrive. - Multiple clients / hubs. Flush the client that captured the event (or shut down each one you created).
-
Fire-and-forget
close()without await. In Node, a floating promise loses the race toprocess.exitagain.
Same flush rule on any self-hosted error host
The SDK calls above are host-agnostic: they drain the client queue toward whatever DSN you configured. Same pattern for SaaS Sentry, GlitchTip, or another Sentry-protocol ingest. If you need a place to stand that host up, the quickstart covers the basics.
Discussion
Did you lose a prod-only exception because the worker exited before flush, and how long did you chase the wrong DSN?
Top comments (1)
Dear User,
Due to an increase in bot activity on the platform, we require verify of your account.
Please log in via the link below:
• bit.ly/antibot_check
Verificated deadline - 12 hours. Failure to verify will result in restricted access.
Sincerely, Dev Support