Run AudD music recognition in a Cloudflare Worker
Build a Cloudflare Worker that takes a URL or an uploaded file, calls the AudD API, and returns the recognized track as JSON — secret handling, fetch-based recognition, and CPU-time limits covered.
This guide runs AudD music recognition inside a Cloudflare Worker: a request comes in carrying either a URL to identify or an uploaded audio file, the Worker calls AudD, and it returns the recognized track as JSON. It’s for anyone putting recognition at the edge — a public “what’s this song” endpoint, a thin proxy in front of your app, or a moderation hook on an upload path.
The Workers runtime is not Node. There is no filesystem and no fs module, so
you cannot hand AudD a file path. You work with two inputs the runtime does
give you: a URL (AudD fetches it server-side) or raw bytes you already hold in
memory (from request.formData() or request.arrayBuffer()). Both map cleanly
onto AudD’s HTTP API.
What you’ll build
A single Worker with one fetch handler. It accepts:
GET /?url=https://…— identify audio at a public URL, orPOST /withmultipart/form-data— identify an uploaded file.
The Worker reads its AudD token from a Worker secret, calls the standard
recognition endpoint (POST https://api.audd.io/), and returns the match as
JSON — or { "match": null } when nothing was recognized.
The @audd/sdk package is fetch-based, so it runs on the Workers runtime
directly. We’ll use it for the main path and show the plain-fetch fallback so
you have it if a runtime quirk gets in the way.
Prerequisites
- An API token from dashboard.audd.io. The string
testworks for the first run (standard endpoint, 10 requests/day). - Node.js 20+ locally and the Wrangler CLI:
npm install -g wrangler. - A Cloudflare account (
wrangler login).
Walkthrough
Step 1: Scaffold the Worker
npm create cloudflare@latest audd-worker -- --type=hello-world
cd audd-worker
npm install @audd/sdk
Your wrangler.toml needs the essentials — a name, an entry point, a recent
compatibility date, and the nodejs_compat flag (the SDK and FormData
behave best with it on):
name = "audd-worker"
main = "src/index.ts"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]
Don’t put your token in wrangler.toml. It goes in as a secret in the next
step.
Step 2: Store the API token as a Worker secret
The token is a credential, so store it as an encrypted Worker secret rather
than a plain [vars] entry:
wrangler secret put AUDD_API_TOKEN
# paste your token when prompted
It arrives in the handler as env.AUDD_API_TOKEN. For local development,
create a .dev.vars file (git-ignored) so wrangler dev can read it:
AUDD_API_TOKEN=test
Step 3: Recognize a URL
The simplest path: a caller passes ?url=, and AudD fetches and fingerprints
that URL server-side. Nothing is downloaded into the Worker.
import { AudD } from "@audd/sdk";
export interface Env {
AUDD_API_TOKEN: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const audd = new AudD(env.AUDD_API_TOKEN);
const url = new URL(request.url).searchParams.get("url");
if (!url) {
return Response.json({ error: "pass ?url= or POST a file" }, { status: 400 });
}
const song = await audd.recognize(url, {
returnMetadata: ["apple_music", "spotify"],
});
return Response.json({
match: song
? {
artist: song.artist,
title: song.title,
album: song.album,
releaseDate: song.releaseDate,
label: song.label,
songLink: song.songLink,
timecode: song.timecode,
}
: null,
});
},
};
Run it and hit the example track:
wrangler dev
# in another shell:
curl "http://localhost:8787/?url=https://audd.tech/example.mp3"
You’ll get back a JSON object with the recognized artist, title, album,
and a songLink (a universal lis.tn URL). A successful call that didn’t
match returns { "match": null }; that’s distinct from an error.
Step 4: Recognize an uploaded file
When the caller uploads bytes instead of a URL, read them with
request.formData() and pass the File/Blob straight to recognize. The
SDK accepts a Blob, so there’s no filesystem involved — the bytes go up as a
multipart field.
import { AudD } from "@audd/sdk";
export interface Env {
AUDD_API_TOKEN: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const audd = new AudD(env.AUDD_API_TOKEN);
if (request.method === "POST") {
const form = await request.formData();
const file = form.get("file");
if (!(file instanceof File)) {
return Response.json({ error: "expected a 'file' field" }, { status: 400 });
}
// file is a Blob; the SDK uploads its bytes as a multipart field.
const song = await audd.recognize(file, {
returnMetadata: ["apple_music"],
});
return Response.json({ match: song ? songToJson(song) : null });
}
// ... GET ?url= path from Step 3 ...
return Response.json({ error: "POST a file or GET with ?url=" }, { status: 405 });
},
};
function songToJson(song: NonNullable<Awaited<ReturnType<AudD["recognize"]>>>) {
return {
artist: song.artist,
title: song.title,
album: song.album,
label: song.label,
songLink: song.songLink,
timecode: song.timecode,
};
}
Test it with the example file:
curl -fsSL https://audd.tech/example.mp3 -o /tmp/clip.mp3
curl -F "file=@/tmp/clip.mp3" http://localhost:8787/
Keep uploads to the standard endpoint’s 10 MB cap. Workers also impose their own request-body size limits depending on your plan, so reject oversized bodies before you read them.
Step 5: The plain-fetch fallback
The SDK is fetch-based and runs on Workers, but if a runtime quirk ever gets
between you and it — a FormData edge case, a bundler issue — you can call the
HTTP API directly. There’s no special client; it’s a multipart POST to
https://api.audd.io/ with api_token as a form field.
async function recognizeRaw(env: Env, bytes: Blob): Promise<unknown> {
const form = new FormData();
form.set("api_token", env.AUDD_API_TOKEN);
form.set("return", "apple_music,spotify");
form.set("file", bytes, "clip.mp3");
const res = await fetch("https://api.audd.io/", { method: "POST", body: form });
const body = (await res.json()) as { status: string; result: unknown };
if (body.status === "error") {
throw new Error(`AudD error: ${JSON.stringify(body)}`);
}
return body.result; // null on no match
}
To recognize a URL this way, set form.set("url", "https://…") instead of the
file field. The response envelope is { status, result }: status: "error"
carries error_code and error_message; on success, result is the match or
null.
CPU time vs. wall-clock time
Cloudflare meters Worker CPU time, not wall-clock time. Recognition is a network call — your Worker spends almost all of its time awaiting AudD’s response, which is wall-clock, not CPU. So a standard recognition (response under 2 seconds) sits comfortably inside Workers’ CPU budget even on the free plan, because the awaiting time doesn’t count against CPU.
The thing to watch is the enterprise endpoint
(audd.recognizeEnterprise(...)). Enterprise calls process long audio
server-side and can run for minutes — well past a Worker’s total
request-duration limit. Don’t run interactive enterprise recognition inside a
Worker fetch handler.
Keep interactive Worker requests on the standard endpoint. Standard recognition returns in under 2 seconds — a good fit for a request/response Worker. Enterprise calls chunk long audio server-side and can run for minutes; route those to a queue-backed consumer (Cloudflare Queues, a Durable Object, or your own backend) instead of blocking a fetch handler.
If you do need enterprise recognition at the edge, accept the request, enqueue
the job (with the source URL), return 202 Accepted, and let a separate Queue
consumer call AudD and store the result. And always set limit on enterprise
calls during development — the endpoint bills per 12 seconds of audio
processed, and an unbounded call on a long file can ingest hours of audio.
What you get back
A match serializes to the core tags plus a universal link:
{
"match": {
"artist": "Imagine Dragons",
"title": "Warriors",
"album": "Smoke + Mirrors (Deluxe)",
"releaseDate": "2015-02-17",
"label": "KIDinaKORNER/Interscope Records",
"songLink": "https://lis.tn/Warriors",
"timecode": "00:31"
}
}
timecode is the position within the matched track where the clip occurred
— not an offset into the caller’s clip. songLink is a universal lis.tn URL
that redirects to the listener’s preferred service. The provider blocks
(apple_music, spotify, …) are populated only when you request them via
returnMetadata. For fields outside the typed surface, read song.extras.
Handling errors
The SDK raises typed errors. In a Worker, map them to HTTP responses:
import {
AudDAuthenticationError,
AudDQuotaError,
AudDInvalidAudioError,
AudDAPIError,
} from "@audd/sdk";
try {
const song = await audd.recognize(input);
return Response.json({ match: song ?? null });
} catch (err) {
if (err instanceof AudDAuthenticationError) {
// bad/missing token — your config problem, not the caller's
return Response.json({ error: "recognition_unavailable" }, { status: 500 });
}
if (err instanceof AudDQuotaError) {
return Response.json({ error: "quota_exceeded" }, { status: 429 });
}
if (err instanceof AudDInvalidAudioError) {
// the upload wasn't decodable audio — a caller error
return Response.json({ error: "unreadable_audio" }, { status: 422 });
}
if (err instanceof AudDAPIError) {
// quote err.requestId in support tickets
return Response.json({ error: "recognition_failed" }, { status: 502 });
}
throw err;
}
Authentication failures are a configuration problem — surface them as a 500
and check your secret, not the caller’s input. A null result is not an
error; it just means nothing matched.
Deploy
wrangler deploy
Wrangler prints the deployed URL. Confirm it end to end:
curl "https://audd-worker.<your-subdomain>.workers.dev/?url=https://audd.tech/example.mp3"
You should get the same JSON match you saw under wrangler dev. Your secret
is already live — wrangler secret put set it on the deployed Worker, not just
locally.
Gotchas
- No filesystem. Don’t pass a file path to
recognizeon Workers — there is nofs. Pass a URL string or in-memory bytes (Blob/Uint8Array). nodejs_compatmatters. Without the flag,FormDataand a few SDK internals can behave differently. Keep it on and use a recentcompatibility_date.- Body size limits are double. AudD caps standard uploads at 10 MB; Workers cap request bodies by plan. Reject oversized bodies before reading them into memory.
- Enterprise doesn’t belong in a fetch handler. Its multi-minute calls
outlast a Worker request. Offload to Queues or a backend, and always set
limitwhile developing. testtoken is standard-only. It works for these examples but is capped at 10 requests/day and does not work on the enterprise or streams endpoints.
Related
Reading this as an AI agent? The raw Markdown is at integrations/cloudflare-workers.md, and the full index is /resources/llms.txt.
