The first time an engineer puts a console.log on a production Worker and doesn't see anything appear anywhere, the instinct is to suspect a bug in the deployment. The log simply disappears. There is no application server with persistent stdout, there is no log file on disk, there is no process that accumulates output between invocations. Each Worker execution happens inside a V8 isolate that is born, processes the request, and disappears — leaving no trace unless you are actively capturing it. Understanding this model is the prerequisite for any observability strategy that truly works at the edge.
wrangler tail: what it is and what it isn't
wrangler tail opens a streaming connection to the Cloudflare infrastructure and retransmits in real time the events of your Workers invocations: metadata of each request (method, URL, response status, CPU duration, country of origin), the entire output of console.log and console.error, exceptions not caught with stack trace, and the result of waitUntil() when it finishes.
What wrangler tail doesn’t do: persist. When you close the session, events that have not yet arrived are lost. Events that happened before you opened the session also do not exist for you. wrangler tail is a live investigation tool—useful for reproducing a problem as you observe it, useless for reconstructing what happened an hour ago.
You can filter the output with --filter-status 500 to see only errors, or --filter-sampling-rate 0.1 to sample 10% of the traffic and not get flooded under high load. In services with hundreds of requests per second, the unfiltered tail quickly makes the terminal unreadable.
The gotcha of silent exceptions
There is a runtime behavior that catches experienced engineers: a Worker can deliver a 200 response to the client and still have thrown an exception — as long as the exception was thrown after the Response was sent.
The concrete scenario: you call ctx.waitUntil(minhaFuncaoAssincrona()) to do background work after the response. If minhaFuncaoAssincrona launches, the customer has already received 200 and is happy. The exception appears in wrangler tail as an error event associated with the request, but does not change the HTTP status that the client saw. Without the tail open, the error is never seen.
The same goes for any promise that you don't await inside the handler. If you fire a fetch() without await and without error handling, and it rejects, the uncaptured rejection appears in the tail but does not affect the response the client received. In a local environment with wrangler dev, this behavior may be different — warnings are more visible. In production, there is total silence.
The defense is systematic: every handler must have a try/catch at the highest level that captures, logs with console.error, and returns an explicit HTTP 500. Every promise passed to waitUntil() must have internal error handling. Not because the runtime will remember you — it won't.
wrangler dev local versus --remote
wrangler dev without flags runs a local server using Miniflare — a Node.js implementation of the Workers runtime. For most development cases, this is sufficient: KV, D1, R2, and Queue bindings work with local in-memory or SQLite implementations, and the feedback loop is immediate without involving the Cloudflare network.
wrangler dev --remote is different: it sends the Worker to the real Cloudflare infrastructure and routes development requests through the real edge. This is necessary when you need to test behaviors that Miniflare cannot faithfully simulate — Durable Objects in production with real state, CDN caching behaviors, or runtime features that the current version of Miniflare has not yet implemented.
The trade-off is obvious: --remote requires [40 authentication, has network latency, and any write operations to KV or D1 go to the real resources (unless you set up separate namespaces/staging banks, which should be standard). Using --remote without staging environment isolation is the shortest path to corrupting production data during development.
Logpush: from ephemeral tail to real retention
For any service that needs to reconstruct what happened after the fact — auditing, post-mortem debugging, analyzing error patterns — wrangler tail is insufficient. Logpush solves this by exporting Workers events to a configured storage destination.
Supported destinations include R2 (Cloudflare's own storage, at $0.05 per million rows exported), S3, Datadog, Splunk, and a few others. Configuration is done via dashboard or API: you choose the dataset (workers-trace-events), the destination, and the fields you want to export — timestamp, event.request.url, event.response.status, event.exceptions, event.logs (which includes the output of console.log).
The important thing about the event.logs field: it captures what you passed to console.log, but as serialized text. If you logged a complex JavaScript object, what arrives in Logpush is the string representation of that object, not structured JSON. For logs that need to be parsed and filtered at the destination — to create alerts in Datadog or queries in Splunk — explicitly serialize to JSON before logging: console.log(JSON.stringify({ level: 'error', message, requestId, stack })). Logpush delivers the string; the target parses the JSON.
What to monitor in production — and what the runtime doesn't deliver
Workers do not emit distributed tracing spans natively. There is no automatic integration with OpenTelemetry, there is no propagation of trace context between subrequests without manual implementation. If a Worker calls three services via fetch() and one of them takes 800ms, wrangler tail shows the total duration but does not break down where the time was spent. To have this visibility, you time it manually: const t0 = Date.now() before the fetch, Date.now() - t0 after, and log the structured result with the name of the service.
Cloudflare acquired Baselime and is integrating its capabilities under the name Cloudflare Observability — log and metrics analysis with more ergonomics than manual tail, but true distributed tracing still requires instrumentation in the code or an SDK that propagates traceparent across subrequests.
The most useful metrics for detecting degradation before the client notices: CPU duration percentile distribution (p50, p95, p99), error rate segmented by route, and count of subrequests per invocation. None come free from the runtime — they need to be built, exported via Logpush, and turned into alerts at the destination. The difference between a service you operate with confidence and one you fear putting into production is almost always the quality of the instrumentation you built before the first incident.
Also read
- Cloudflare Workers in production: what changes after hello world
- Observability in Distributed Systems: Logs, Metrics and Tracing
- Observability beyond logs: what OpenTelemetry changes in practice
- Workers + D1 + KV + R2: composing bindings in the same service
- Workers: CPU and memory limits — which the documentation doesn't explain well
- Testing Workers: unit, integration and how to simulate the runtime without depending on Cloudflare
