Compute · Observability

See what your Workers are actually doing

After this lesson you'll be able to stream live logs from a running Worker with wrangler tail, turn on persistent Workers Logs in the dashboard, and know when you need Logpush instead.

You can't attach a debugger to a Worker running on Cloudflare's network. There's no SSH, no server to tail a file on, no single box to log into — a request might execute on any of hundreds of data centers. Observability is how you get visibility anyway: a built-in pipeline that captures console.log() output, invocation metadata, and uncaught exceptions, and gives you three ways to look at it — live in a terminal, stored and searchable in the dashboard, or exported to your own systems.

Three tools, three time horizons. wrangler tail is right now — a live stream you watch while reproducing a bug, gone when you close the terminal. Workers Logs is the last few days — persisted, filterable, searchable in the dashboard. Logpush is forever, elsewhere — a continuous export to storage or a third-party log platform you already use. Most workflows use the first two; Logpush is for when compliance or existing tooling requires it.

How it works

Real-time logs: wrangler tail

Running wrangler tail opens a live connection to your deployed Worker and streams every invocation to your terminal as it happens — request metadata, anything passed to console.log(), and any exceptions, as structured JSON (or a human-readable "pretty" format). It streams nothing retroactively; it only shows requests that happen while it's connected. A single Worker can have up to 10 concurrent tail viewers, and under heavy traffic Cloudflare samples the stream rather than dropping the connection, so at high volume you're seeing a representative slice, not literally every request.

You can filter the stream so you're not scrolling past noise:

npx wrangler tail my-worker --status error
npx wrangler tail my-worker --method POST --search "checkout"
npx wrangler tail my-worker --format pretty

Common flags: --status (ok / error / canceled), --method, --header, --ip, --search (text match against your console.log output), --sampling-rate, and --version-id to isolate one deployed version.

Persistent logs: Workers Logs

Workers Logs is the dashboard-backed version of the same data — enabled per-Worker, stored server-side, and queryable after the fact instead of only while you're watching. Turn it on in your Wrangler config:

// wrangler.jsonc
{
  "name": "my-worker",
  "main": "src/index.ts",
  "observability": {
    "enabled": true,
    "head_sampling_rate": 1
  }
}

head_sampling_rate is a number from 0 to 1 — 1 means log every invocation, 0.1 means log roughly 10%. Turning it down is how you control both log volume and cost on a busy Worker. Once enabled (Wrangler 3.78.6+), logs show up in the dashboard under your Worker's Logs tab, where you can filter and query them with a builder instead of grepping a terminal stream — useful once you're past "watch it happen live" and into "find the three requests from yesterday that 500'd."

Export: Logpush and Tail Workers

Two mechanisms move telemetry outside Cloudflare's own dashboard. Logpush is a managed, continuous export of Workers trace events to a destination — R2, S3, or a third-party log platform — configured once and running in the background, no code required. Tail Workers are code: a separate Worker you write that receives the execution events of another "producer" Worker (status, console output, exceptions) and can transform, filter, sample, or forward them programmatically. Wire one up by adding it to the producer's config:

// wrangler.jsonc for the producer Worker
{
  "tail_consumers": [
    { "service": "my-tail-worker" }
  ]
}

Reach for Logpush when you want "send everything to Datadog/Splunk/R2" with no custom logic. Reach for a Tail Worker when you need to react to events in code — e.g., page on-call only when error rate crosses a threshold, or redact a field before it leaves Cloudflare. Tail Workers are a Paid/Enterprise plan feature.

Metrics and analytics

Separately from logs, every Worker gets an automatically-populated Metrics view in the dashboard: request volume split into success/error/subrequests, wall time and CPU time per invocation (as quantiles), invocation outcome (success, exceeded resources, threw exception, client disconnected — not the same thing as an HTTP status code), and subrequest cache hit/miss counts. This data is powered by GraphQL under the hood, so you can also query it programmatically instead of reading the dashboard chart by eye. Metrics need no configuration — they exist for every Worker whether or not you've turned on Workers Logs.

Pricing

Confirmed from Cloudflare's Workers platform pricing docs as of this writing:

FeatureFree planPaid plan
Workers Logs volume200,000 logs/day included20,000,000 logs/month included, then $0.60 per additional million
Workers Logs retention3 days7 days
Logpush (Workers Trace Events)Not available10,000,000 logs/month included, then $0.05 per additional million
Tail WorkersNot availableIncluded (Paid/Enterprise)
wrangler tail / real-time logsIncludedIncluded

Also worth knowing: each individual log entry is capped at 256 KB (larger entries are truncated), and there's an account-wide ceiling of 5 billion logs/day regardless of plan. Logpush bills on logs that actually reach the destination after your filtering/sampling rules, not on raw volume. Pricing changes — verify current numbers on the live pricing page before budgeting against them.

Use cases

Worked example

A minimal Worker with structured logging, watched live, then made persistent.

// src/index.ts
export default {
  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);
    const start = Date.now();

    try {
      const orderId = url.searchParams.get("orderId");
      if (!orderId) {
        console.log(JSON.stringify({
          level: "warn",
          event: "missing_order_id",
          path: url.pathname,
        }));
        return new Response("Missing orderId", { status: 400 });
      }

      const result = await processOrder(orderId);

      console.log(JSON.stringify({
        level: "info",
        event: "order_processed",
        orderId,
        durationMs: Date.now() - start,
      }));

      return Response.json(result);
    } catch (err) {
      console.log(JSON.stringify({
        level: "error",
        event: "order_processing_failed",
        message: (err as Error).message,
        durationMs: Date.now() - start,
      }));
      return new Response("Internal error", { status: 500 });
    }
  },
};

async function processOrder(orderId: string) {
  // ... real logic here
  return { orderId, status: "confirmed" };
}

Deploy it, then watch it live while you hit it with test traffic:

npx wrangler deploy
npx wrangler tail --search "order_processing_failed"

Once you're confident it's behaving, enable persistent Workers Logs so the same structured entries are still there tomorrow:

// wrangler.jsonc
{
  "observability": { "enabled": true, "head_sampling_rate": 1 }
}

Because every log line is a JSON object with an event field, you can now filter in the dashboard's query builder by event = "order_processing_failed" instead of full-text-searching a wall of prose.

Pitfall: unstructured console.log doesn't scale. console.log("order failed", orderId, err.message) works fine when you're staring at a live tail stream during development. Once that Worker is handling real traffic and those lines pile up in Workers Logs, plain-text messages aren't reliably filterable or aggregable — you end up grepping strings and hoping the format didn't drift between deploys. Log a structured object (console.log(JSON.stringify({ event, orderId, ... }))) from day one so every field is independently queryable later. Retrofitting structure after you already have a production incident to debug is the wrong time to learn this.
Primary source

Cloudflare Docs — Workers Observability is the canonical entry point for logs, tail, tracing, and metrics, and links out to the dedicated Workers Logs, real-time logs, Tail Workers, and Logpush pages used for this lesson. Pricing was checked against Workers Platform Pricing.

You want to catch a bug that's happening intermittently in production right now, so you can see the exact request that triggers it. Which tool fits best?
Without scrolling up: what's the practical difference between Workers Logs and Logpush, and why would logging console.log("failed", id) as a plain string instead of structured JSON bite you later?
Reveal

Workers Logs stores your Worker's logs in Cloudflare's own dashboard for a short retention window (3 days free, 7 days paid) so you can filter and query them after the fact. Logpush continuously exports the same kind of telemetry to a destination you control — R2, S3, or a third-party platform — for longer retention or integration with existing tooling, and is a paid-plan-only feature billed separately per log delivered.

Plain-string logs like console.log("failed", id) read fine in a live wrangler tail session, but once they accumulate in Workers Logs at volume, there's no reliable field to filter or aggregate on — you're reduced to text search and hoping the message format never changed. Logging a JSON object instead ({ event: "failed", orderId: id }) makes every field independently queryable in the dashboard's query builder.

Anything above unclear — how sampling rates interact with cost, when you'd reach for a Tail Worker instead of Logpush, or how metrics quantiles are computed? Ask your AI teacher before moving on.
← Previous: Real-time coordination: WebSockets with Durable Objects Next: Let your users run their own code on your platform →