Response times rarely break all at once; they drift, and a Datadog dashboard wired to your Webflow Cloud metrics is what turns that drift into something you can see.
Logs tell you what happened. Metrics tell you whether it's happening at the right rate. I've run Webflow Cloud projects where logs were clean but response times silently degraded over two weeks.
You don't see that in logs without a specific query; a latency gauge on a dashboard shows it instantly as an upward slope. I've caught three silent degradations this way before any user filed a report.
The approach is a fetch-based metrics utility that submits to Datadog's v2 series API from your Route Handlers, plus a Datadog dashboard wired to those metrics. The dd-trace Node.js APM library doesn't run on Cloudflare Workers, but the Datadog Metrics API over fetch does, and that is the whole setup.
Here's what you need before wiring it up.
What do you need to track Webflow Cloud app metrics with Datadog Dashboards?
You need a Webflow Cloud Next.js project and a Datadog account. Metrics and logs use the same key but different endpoints.
Here’s the full list:
- A Webflow Cloud project running a Next.js app, deployed or in local dev
- A Datadog account with an API key (app.datadoghq.com)
- Node.js 22 or higher locally
The metrics setup has no additional npm packages. Everything runs on native fetch.
5 steps to track Webflow Cloud app metrics with Datadog Dashboards
The approach covers three metric types:
- Request counts (how many requests hit each Route Handler)
- Error counts (how many failed)
- Latency gauges (how long each request took)
These three metrics are enough to build a Datadog dashboard that answers "Is my Webflow Cloud app healthy right now?"
1. Get a Datadog API key and set the Webflow Cloud environment variables
Log in to app.datadoghq.com. Go to Organization Settings > API Keys > New Key. Name it webflow-cloud-metrics. Copy the key.
Then find your Datadog site, which is not on the API Keys page: it is listed at the top of My Preferences, and the quickest tell is the hostname in your browser while logged in. Datadog runs nine independent sites (US1, US3, US5, EU1, AP1, AP2, UK1 and two government sites), each with its own API hostname.
They cannot share data, so valid metrics sent to the wrong site land in an organization you are not looking at rather than erroring.
Add both to .env.local and to Webflow Cloud's Settings > Environment Variables:
DD_API_KEY=your_datadog_api_key_here
DD_SITE=datadoghq.com
DD_SITE defaults to datadoghq.com (US1). Check your account region and update if needed.
2. Build a Datadog metrics utility for Webflow Cloud
Create lib/metrics.ts. This utility wraps the Datadog v2 series API and exposes three functions: trackRequest, trackError, and trackLatency.
All three hand their send to ctx.waitUntil() rather than fire-and-forget. That distinction matters more than it looks: Cloudflare documents that "an async call that is neither awaited nor passed to ctx.waitUntil() can be canceled when the invocation ends; dropping logs, leaving writes unfinished, or failing silently."
A bare void fetch(...) is exactly that call, so the metrics you cannot find later may never have been sent at all.
waitUntil extends the Worker's lifetime past the response, giving you both properties you want: the response isn't delayed, and the send actually completes.
// lib/metrics.ts
type MetricType = 1 | 3 // 1 = count, 3 = gauge
type DatadogPoint = {
timestamp: number
value: number
}
type DatadogSeries = {
metric: string
type: MetricType
points: DatadogPoint[]
tags: string[]
// Datadog requires `interval` (in seconds) for count and rate
// metrics. Omit it and your counts are not stored the way the
// dashboard queries assume.
interval?: number
resources?: { name: string; type: string }[]
}
export async function submitMetric(series: DatadogSeries[]): Promise<void> {
// Read the site per call, not at module scope: on this adapter
// process.env is populated per request, so a module-level read can
// silently fall back to US1 and ship your metrics to an org you
// are not looking at.
const site = process.env.DD_SITE ?? 'datadoghq.com'
await fetch(`https://api.${site}/api/v2/series`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'DD-API-KEY': process.env.DD_API_KEY!,
},
body: JSON.stringify({
series: series.map((s) => ({
...s,
resources: s.resources ?? [{ name: 'webflow-cloud', type: 'host' }],
})),
}),
})
}
type RouteContext = {
route: string
method: string
status: number
env?: string
}
function buildTags(ctx: RouteContext): string[] {
return [
`route:${ctx.route}`,
`method:${ctx.method}`,
`status:${ctx.status}`,
`env:${ctx.env ?? process.env.NODE_ENV ?? 'development'}`,
]
}
// Counts carry an interval; gauges do not.
const COUNT_INTERVAL = 60
// Count: one request hit this route.
export function requestSeries(ctx: RouteContext): DatadogSeries {
return {
metric: 'webflow.app.request.count',
type: 1, // count
interval: COUNT_INTERVAL,
points: [{ timestamp: Math.floor(Date.now() / 1000), value: 1 }],
tags: buildTags(ctx),
}
}
// Count: one error on this route.
export function errorSeries(ctx: RouteContext): DatadogSeries {
return {
metric: 'webflow.app.error.count',
type: 1, // count
interval: COUNT_INTERVAL,
points: [{ timestamp: Math.floor(Date.now() / 1000), value: 1 }],
tags: buildTags(ctx),
}
}
// Distribution: response time in milliseconds. See the note below on
// why this is not a gauge.
export function latencySeries(ctx: RouteContext, durationMs: number): DatadogSeries {
return {
metric: 'webflow.app.latency.ms',
type: 3, // gauge
points: [{ timestamp: Math.floor(Date.now() / 1000), value: durationMs }],
tags: buildTags(ctx),
}
}
// Batch the series into one call. Returns the promise so the caller
// can hand it to ctx.waitUntil rather than dropping it.
export function trackAll(
ctx: RouteContext,
durationMs: number,
isError: boolean
): Promise<void> {
const series: DatadogSeries[] = [
requestSeries(ctx),
latencySeries(ctx, durationMs),
]
if (isError) {
series.push(errorSeries(ctx))
}
return submitMetric(series)
}
trackAll batches two series on a successful request and three when there was an error, in a single call. Batching matters on this platform for a reason that is easy to miss: Webflow Cloud allows only six simultaneous outgoing requests per invocation, against a 20-second request timeout.
A handler that already calls an API and a database has little headroom left, and three separate telemetry fetches compete with the work the request actually exists to do. Datadog does not rate-limit metric submission, so the constraint here is the platform's, not the vendor's.
Metric naming follows the Datadog convention: dot-separated, lowercase, starting with a namespace. webflow.app.* groups all Webflow Cloud app metrics under one prefix in the Datadog Metrics Explorer.
3. Add Datadog metrics to Webflow Cloud Route Handlers
The pattern is: record the start time at the top of the handler, resolve the status at the bottom, and call trackAll with the result.
Wrap the whole handler in a try/catch block so errors are also tracked:
// app/api/orders/route.ts
import { getCloudflareContext } from '@opennextjs/cloudflare'
import { trackAll } from '@/lib/metrics'
import { NextResponse, type NextRequest } from 'next/server'
export async function POST(request: NextRequest) {
const startTime = Date.now()
let status = 200
const { ctx } = getCloudflareContext()
try {
await request.json()
// ... your order processing logic
return NextResponse.json({ ok: true })
} catch {
status = 500
return NextResponse.json({ error: 'Failed to process order' }, { status: 500 })
} finally {
// waitUntil keeps the Worker alive until the send completes,
// without holding up the response. Dropping the promise here
// instead would let the runtime cancel it.
ctx.waitUntil(
trackAll(
{
route: '/api/orders',
method: 'POST',
status,
},
Date.now() - startTime,
status >= 500
)
)
}
}
The finally block is the right place for the call, because it runs whether the handler succeeded or threw, so you get metrics for failures as well as successes.
Be precise about the ordering, though, because it is commonly misdescribed. A finally block runs before the function actually returns: JavaScript evaluates the return value, executes finally to completion, and only then hands control back. So finally doesn't keep this off the response path. waitUntil is.
It accepts the promise, lets the response go out, and keeps the invocation alive until the send finishes.
For Route Handlers that handle multiple HTTP methods, extract the metric call into a shared wrapper:
// lib/withMetrics.ts
import { getCloudflareContext } from '@opennextjs/cloudflare'
import { trackAll } from '@/lib/metrics'
import { type NextRequest, NextResponse } from 'next/server'
// This wrapper is for static routes. Next.js passes a second argument
// carrying `params` to dynamic routes like app/api/orders/[id]/route.ts,
// and this signature discards it.
type RouteHandler = (request: NextRequest) => Promise<NextResponse>
export function withMetrics(route: string, handler: RouteHandler): RouteHandler {
return async (request: NextRequest) => {
const { ctx } = getCloudflareContext()
const startTime = Date.now()
let status = 200
try {
const response = await handler(request)
status = response.status
return response
} catch (error) {
status = 500
return NextResponse.json({ error: 'Internal error' }, { status: 500 })
} finally {
ctx.waitUntil(
trackAll(
{ route, method: request.method, status },
Date.now() - startTime,
status >= 500
)
)
}
}
}
Use it in any Route Handler:
// app/api/products/route.ts
import { withMetrics } from '@/lib/withMetrics'
import { NextResponse, type NextRequest } from 'next/server'
async function handler(request: NextRequest): Promise<NextResponse> {
// ... handler logic
return NextResponse.json({ products: [] })
}
export const GET = withMetrics('/api/products', handler)
With the wrapper in place, every request that passes through these handlers emits a request count, a latency gauge, and an error count on failure, without repeating the timing logic in each file.
A handler wrapped with withMetrics and one instrumented inline submit to the same three metrics, so they show up side by side in Datadog. Next, confirm those metrics are actually arriving.
4. Verify Datadog is receiving Webflow Cloud metrics
Send a test request to one of your instrumented Route Handlers in the deployed Webflow Cloud environment. Give Datadog 60-90 seconds to process the submission.
Go to Metrics > Explorer in the Datadog dashboard. In the metric search box, type webflow. If the metrics are arriving, you'll see webflow.app.request.count, webflow.app.error.count, and webflow.app.latency.ms in the autocomplete list.
Click webflow.app.request.count and change the time window to Past 15 Minutes. You should see a bar corresponding to the request you just sent.
If no metrics appear after about two minutes, see the troubleshooting section.
5. Build a Datadog Dashboard for your Webflow Cloud app
Go to Dashboards > New Dashboard and name it "Webflow Cloud App." When prompted for a layout, choose Dashboards, which is the grid-based option. There is no control labeled "Grid"; the alternatives are Timeboards and Screenboards.
Add these four widgets to cover the core health indicators:
Widget 1: Request volume over time (Timeseries)
Click Add Widget > Timeseries.
In the Metric field, enter:
sum:webflow.app.request.count{*} by {route}
Set the display to Bars and the title to "Requests by Route." This shows how many requests each Route Handler receives per minute.
Widget 2: Error rate (Timeseries)
Add another Time series widget:
sum:webflow.app.error.count{*} by {route}
Set display to Lines, color to red, and title to "Errors by Route." Stack this below the request volume widget so request spikes and error spikes align visually.
Widget 3: Average latency (Timeseries)
Add a Timeseries widget:
avg:webflow.app.latency.ms{*} by {route}
Title: "Average Latency (ms) by Route." For latency, avg is the right aggregation for most use cases. If you need P95 or P99 percentiles, submit distribution points instead of gauge metrics.
Widget 4: Top routes by request count (Toplist)
Add a Toplist widget:
sum:webflow.app.request.count{*} by {route}
Title: "Top Routes." This shows your busiest endpoints at a glance.
Save the dashboard. Set the default time window to Past 4 Hours; that's the right resolution for catching degradations before they compound.
Set up a latency alert: Go to Monitors > New Monitor > Metric.
Configure:
avg:webflow.app.latency.ms{*} > 1000
Alert when this condition holds for 5 minutes. That threshold catches sustained latency problems without alerting on transient spikes. Route it to Slack or PagerDuty.
What breaks Datadog metrics tracking in Webflow Cloud?
Most metric problems on Webflow Cloud trace back to one of four causes: a send canceled before it left the Worker, a missing or misconfigured API key, metrics pointed at the wrong Datadog site, or a gauge being asked for a percentile it cannot produce.
Each fails quietly because submission happens off the response path and never reaches your Route Handler.
The symptoms below map to those causes, starting with the one that produces the most confusing result: metrics that arrive but are read wrong.
Metrics appear in Metrics Explorer, but the values are unexpectedly low
Two causes are worth separating, because the utility above already rules out the one people reach for first.
Timestamps are not it. The utility calls Math.floor(Date.now() / 1000), and a millisecond timestamp would not produce low values anyway: Datadog accepts a point only within ten minutes ahead or an hour behind, so a millisecond value lands tens of thousands of years out and is discarded. That symptom is missing data, not low data.
Low values usually mean gauge collapse. Many requests within one interval share identical tags, and a gauge retains a snapshot rather than an aggregate, so per-request values don't accumulate the way a count does.
The utility uses Math.floor(Date.now() / 1000); confirm that the conversion is in place. If you accidentally pass milliseconds as the timestamp, Datadog rejects the point as "more than 10 minutes in the future" and silently drops it.
No metrics appear in Metrics Explorer after a couple of minutes
Check two things. First, confirm DD_API_KEY is set in the environment variables for that Webflow Cloud environment. A missing key fails silently on your app's side because the send runs off the response path: Datadog returns a 403 for a bad key, and nothing in your handler listens for it. Log the response status once while debugging.
Add a temporary console.error inside submitMetric to surface failures, check Webflow Cloud logs, then remove it before shipping.
Second, confirm DD_SITE matches your Datadog account region. If you're on EU1 (datadoghq.eu) but DD_SITE is set to datadoghq.com, metrics go to US1 and won't appear in your EU1 account.
The same metric appears twice with different values
Two Route Handlers are submitting metrics with the same metric name but without distinguishing tags. Check that the route is always set to the specific handler path (e.g., /api/orders, not /api).
If two handlers share a metric name without a differentiating tag, Datadog sums them, and you can't tell them apart.
Latency values look correct, but the dashboard shows no P95 percentile option
Gauge metrics don't support percentile aggregations in Datadog. To get P50/P95/P99, submit distribution points instead of gauge points, using the POST /api/v1/distribution_points endpoint on your site's API host.
Switching the submission is only half the battle, which is why people try this and still don't see a percentile option. Percentile aggregations on a distribution metric are off by default and must be enabled for that specific metric on the Metrics Summary page. Do both, or nothing changes.
The payload format also differs from v2/series, and distributions cost meaningfully more (see the billing note in the FAQ). For most Webflow Cloud projects, avg is sufficient; switch when percentile visibility genuinely matters.
Extend Datadog Dashboard metrics in your Webflow Cloud app
Dashboards answer "what is happening now." The two natural companions are alerting, so a threshold reaches you without anyone watching a screen, and logs, so you can see why a spike happened.
If you already run Sentry for exceptions, our Sentry and Datadog guide covers adding Datadog logging alongside it without duplicating what each tool does well.
Explore Webflow + Datadog for client-side Real User Monitoring and Synthetic checks, then pair that browser-side view with the server-side request, latency, and error metrics from this guide to watch a Webflow Cloud app end to end.
Frequently asked questions
What's the difference between tracking Datadog metrics and shipping Datadog logs?
Logs are discrete text events: a Route Handler processed order 12345, or a signature validation failed. Metrics are numeric time series: 47 requests per minute to /api/orders, with an average latency of 82ms. Use logs for debugging specific incidents. Use metrics to detect patterns, set alerts, and understand trends over time. They're complementary; I run both on every Webflow Cloud project.
Will submitting metrics on every request affect my Route Handler response time?
No, as long as you hand the promise to ctx.waitUntil() as shown. That is what waitUntil is for: the response goes out immediately, and the Worker stays alive just long enough to finish the send.
Resist the shortcut of dropping the promise with void submitMetric(...). It looks equivalent, but Cloudflare documents that an async call which is neither awaited nor passed to waitUntil can be canceled when the invocation ends, which means the metrics you most want during an incident are the ones most likely to disappear.
How many custom metrics can I submit before Datadog starts billing me?
Do not plan around a free allowance, because there isn't one. Datadog's documented allotments are 100 custom metrics per host on Pro and 200 per host on Enterprise; the billing documentation lists no free-tier allocation.
The counting rule is what makes this bite: one custom metric is one unique combination of metric name and tag values, including the host tag. The number is multiplicative in your tags rather than additive, and the status tag is the one to watch, because every distinct status code a route returns adds another combination, and that set is open-ended.
Can I use the same Datadog API key for both metrics and log ingestion?
Yes. The Datadog API key is scoped to the organization, not to a specific data type. The same key works for the Metrics API (/api/v2/series) and the Log Intake API (/api/v2/logs). If you've already set DD_API_KEY as described in the logging guide, you don't need additional keys.
What's the best way to monitor Webflow Cloud Datadog metrics outside business hours?
Configure latency and error-rate monitors to route to PagerDuty for on-call escalation during off-hours and to Slack during business hours only. Use Datadog's notification scheduling in the monitor settings to manage routing by time of day. For a Webflow Cloud app where availability is critical, a 99.5% uptime SLO with an associated Datadog monitor typically covers most incident-detection needs.




