Logs
npx wawesome logs lists your Function’s recent runs, prints what one of them wrote, and follows
output as it is produced. Each run is an invocation: a request arrives, your handler answers, the
run ends. Concepts has the longer definition.
Every run captures whatever your code sent to stdout and stderr. That body is kept for 14 days.
Recent invocations
Section titled “Recent invocations”npx wawesome logs with nothing after it reads the wawesome-function.json in the current directory.
Pass a Function name to read one from anywhere.
npx wawesome logs charge
📜 Invocations for 'shop/charge' (Showing 4 of 4 records)
INVOCATION ID | STATUS | TRIGGER | STARTED AT | DURATION
------------------------------------|-----------|---------|---------------------|----------
6b1f9c2e-4d3a-4f8b-9c11-2a7e5d0b8f34 | success | http | 2026-09-08 09:12:04 | 128ms
a4d7e310-8b22-4c6f-91de-5f0c3a19b7e6 | success | http | 2026-09-08 09:11:47 | 41ms
2f8c5b91-7e04-4a3d-8b6c-1d9f4e2a0c53 | success | manual | 2026-09-08 09:07:15 | 1.42s
ce03a6d4-19f7-4b8e-a2c5-6d81b0f9e374 | timeout | cron | 2026-09-08 09:00:00 | 30.01s
To view logs for a specific invocation, run:
wawesome logs --invocation <id>
To follow live output (waits for the next invocation if none is running):
wawesome logs <function-name> --follow
To follow a specific invocation:
wawesome logs --invocation <id> --follow
The 50 most recent runs, newest first. TRIGGER says what started each one: http for a caller,
cron for a schedule, manual for a run you fired with invoke.
STATUS is not the HTTP status code. A run a caller pulled is success once it answered and
survived its own body, whatever code it chose. So the second row above answered 500 and still
reads success. error means we stopped the run before it finished. timeout means it ran past
its budget, and running means it has not ended yet.
--error, --success, --timeout and --running filter on that column, and --status <status>
takes the same four words. So --error finds traps and background runs that answered outside the
2xx range, and it never finds a 500 a handler returned to a caller.
One run’s output
Section titled “One run’s output”An invocation id fetches the body that run captured.
npx wawesome logs --invocation a4d7e310-8b22-4c6f-91de-5f0c3a19b7e6
{"ts":"2026-09-08T09:11:47.204118Z","stream":"stdout","msg":"Log: charging 4210 for cus_9f2"}
{"ts":"2026-09-08T09:11:47.241902Z","stream":"stderr","msg":"Error while running request handler: charges is not defined"}
{"ts":"2026-09-08T09:11:47.241940Z","stream":"stderr","msg":"Stack:"}
{"ts":"2026-09-08T09:11:47.241944Z","stream":"stderr","msg":" fetch@./user.js:6:20"}
{"ts":"2026-09-08T09:11:47.241947Z","stream":"stderr","msg":" @./index.js:5:42"}
{"ts":"2026-09-08T09:11:47.241951Z","stream":"stderr","msg":" @./index.js:5:63"}
One JSON object per line. ts is when we read that line off your Function, to the microsecond, so a
tight logging loop keeps its real order. stream is stdout or stderr. msg is the line itself.
The id goes in front of the flag just as well. That is the shorter thing to paste:
npx wawesome logs a4d7e310-8b22-4c6f-91de-5f0c3a19b7e6
An id from another workspace, one that never existed, and one older than 14 days are all the same answer:
[wawesome] Error: Invocation log 'a4d7e310-8b22-4c6f-91de-5f0c3a19b7e6' not found or expired (logs are retained for 14 days).
A row outlives the body it points at. The listing keeps a run for 35 days and its output for 14. A run from three weeks ago still has a row with its status and its duration, and nothing left to print.
Where an id comes from
Section titled “Where an id comes from”Every response that became an invocation carries its own id back in the x-wawesome-invocation-id
header.
HTTP/2 500
x-wawesome-invocation-id: a4d7e310-8b22-4c6f-91de-5f0c3a19b7e6
That is what makes a bug report worth having. Whoever hit the failing request reads the header off their own response and hands you one id. You read that exact run instead of guessing which of 50 rows was theirs.
Which lines are yours
Section titled “Which lines are yours”Nearly all of it is your own output. The rest comes from the engine and from us.
The engine labels every console call with the method that made it, so console.log('ready') is
stored as Log: ready. console.info, console.warn and console.error become Info:, Warn: and
Error:. All four go to stdout, so a console.error of yours is not a stderr line.
The engine also writes when it catches something your code did not. That is the Error while running request handler: line above, and the Stack: block under it. Frames naming ./user.js are your
module. That is the filename your entry point is deployed as, and the line and column are your own
source. Frames naming ./index.js are the wrapper we generate around your module to route a trigger
into your fetch handler. There is nothing to fix in those.
Lines starting Fatal: are ours, and there are five of them:
| Line | What stopped the run |
|---|---|
Fatal: Execution trapped |
The instance died. A detail follows it where the engine did not already print one |
Fatal: Execution Time Limit Exceeded |
The run passed its invocation budget |
Fatal: Compute Budget Exceeded |
The run passed its compute budget |
Fatal: Response Size Limit Exceeded |
The response grew past what one invocation may return |
Fatal: No room to start the Function |
The host had no room to start the run, so the caller got a 503. Try again shortly |
A Fatal: line is always the last line, because the run ended on it.
Following live output
Section titled “Following live output”--follow on a Function name streams that Function’s output as it is produced, across runs.
npx wawesome logs charge --follow
[wawesome] 📡 Following function shop/charge (Ctrl-C to stop)...
Then it waits. Nothing more is printed until the Function is invoked, and it makes no difference whether a run was already going when you started. Hit the URL from another window and the lines arrive:
Log: charging 4210 for cus_9f2
Log: charged 4210 for cus_9f2
Following prints msg on its own, without the timestamp and the stream, and colors stderr lines
red. Read the run back with --invocation afterwards for the timestamps.
The stream stays open across runs, so the next invocation prints under the last one. Ctrl-C stops it:
[wawesome] Stopped following.
The console follows a Function the same way. On its Invocations tab, Live output at the right of the bar swaps the rows for the output as it arrives. Switch it off and the rows come back as you left them.
You hold one live tail at a time across the whole platform. Open a second one anywhere and the first ends. Worth knowing before you go hunting for why a terminal you left open went quiet.
--follow with an invocation id follows that one run instead, and returns when it ends:
npx wawesome logs 6b1f9c2e-4d3a-4f8b-9c11-2a7e5d0b8f34 --follow
[wawesome] 📡 Following invocation 6b1f9c2e-4d3a-4f8b-9c11-2a7e5d0b8f34 (Ctrl-C to stop)...
Log: charging 4210 for cus_9f2
[wawesome] ✔ Live tail ended for 6b1f9c2e-4d3a-4f8b-9c11-2a7e5d0b8f34.
A run that has already finished has nothing left to stream, so that prints the two [wawesome] lines
and no output at all. Drop --follow to read a finished run.
Fire a run and read it in one step
Section titled “Fire a run and read it in one step”invoke starts a run through the authenticated API and follows its output, without waiting for a
caller or for a schedule to come round.
npx wawesome invoke charge
[wawesome] ⏳ Run 2f8c5b91-7e04-4a3d-8b6c-1d9f4e2a0c53 queued for 'shop/charge'...
Log: charging 4210 for cus_9f2
Log: charged 4210 for cus_9f2
[wawesome] ✔ Run completed: success (1.42s)
The run reaches your handler as a POST with no body. -m sets the method and -d sets the body:
npx wawesome invoke charge -m POST -d '{"customer":"cus_9f2"}'
--no-follow fires the run and exits on the queued line, leaving you the id to read later.
An invoked run is a background run, which is why it lists as manual rather than http. That also
changes what the record says about it: a background run is success only when it answered 2xx, and
error for anything else.
[wawesome] ✖ Run completed: error (38ms)
So one bug reads two ways. The 500 in the logs listing above is success on its http row and
error on the manual row of the same handler failing the same way. Firing invoke at a handler you
suspect is the quickest way to make a caller-facing 500 show up as a failure.
The 500 people misread
Section titled “The 500 people misread”Your handler fails in two ways, and we record them differently. Telling them apart is the difference between one bad request and a Function that is answering nothing at all.
A failure inside the handler is a 500
Section titled “A failure inside the handler is a 500”A throw inside your handler, or a promise it awaits that rejects, is caught by the engine. The engine
answers 500 on your Function’s behalf, with an empty body.
export default {
async fetch(request) {
const customer = request.headers.get('x-customer');
console.log(`charging 4210 for ${customer}`);
const charge = await charges.create(customer, 4210);
return Response.json(charge);
},
};
The caller gets a 500 and no x-wawesome-error header. That absence is the signal. A status with
no class beside it is your Function’s own answer, so we did not stop this run. On the wire it is
indistinguishable from return new Response(null, { status: 500 }). The row in logs says
success, because the run did answer.
The log body is where the two come apart:
{"ts":"2026-09-08T09:11:47.241902Z","stream":"stderr","msg":"Error while running request handler: charges is not defined"}
{"ts":"2026-09-08T09:11:47.241944Z","stream":"stderr","msg":" fetch@./user.js:6:20"}
Your handler ran. It reached line 6 and threw. Catch it there and answer the status you mean, and the caller gets something more useful than an empty 500.
Do not answer 502 or 504. The caller gets a page that is not yours in their place, with neither
your body nor your headers. When a service you call fails, answer 500.
A failure at module scope is a trap
Section titled “A failure at module scope is a trap”Code outside the handler runs once, when the module is evaluated, before any request reaches your
fetch. A throw there kills the instance.
const key = process.env.STRIPE_KEY;
if (!key) throw new Error('STRIPE_KEY is not set');
export default {
async fetch(request) {
return new Response('ok');
},
};
The caller gets a 500 with x-wawesome-error: trap and the message The Function failed to run.
The row in logs says error. The body carries the engine’s own account, and then our line under
it:
{"ts":"2026-09-08T09:11:47.198004Z","stream":"stderr","msg":"Exception while evaluating top-level script"}
{"ts":"2026-09-08T09:11:47.198091Z","stream":"stderr","msg":"./user.js:2:17 Error: STRIPE_KEY is not set"}
{"ts":"2026-09-08T09:11:47.198119Z","stream":"stderr","msg":"Stack:"}
{"ts":"2026-09-08T09:11:47.198126Z","stream":"stderr","msg":" @./user.js:2:17"}
{"ts":"2026-09-08T09:11:47.198140Z","stream":"stderr","msg":"Fatal: Execution trapped"}
Your handler never ran, and it will not run for the next request either. Every call fails the same way until you deploy a fix. A trap is an outage. A 500 is one request going wrong.
Only a module-level throw traps. Nothing you can call from inside a handler will, because no API we give you aborts the instance. A failure in there is always the 500 above.
Reading the difference
Section titled “Reading the difference”| Threw in the handler | Threw at module scope | |
|---|---|---|
| What the caller gets | 500, empty body | 500, The Function failed to run |
x-wawesome-error |
absent | trap |
STATUS in logs |
success |
error |
| First line to read in the body | Error while running request handler: |
Exception while evaluating top-level script |
| Requests affected | this one | every one, until you deploy |
The third row is why --error is the wrong place to look for a 500 somebody reported. Ask them for
the x-wawesome-invocation-id off their response and read that run directly.
The console does find them. On a Function’s Invocations tab, Failed lists every run that failed
whoever asked: one that never answered, and one that answered 500 or above, whatever the trigger.
A 4xx is not in it, because that is the Function saying the request was wrong. Open a row to read
the last lines of its output, and All failures like this to see the rest.