---
title: "Run AudD music recognition in a Cloudflare Worker"
description: "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."
slug: "/resources/integrations/cloudflare-workers"
section: "integrations"
keywords: [audd, cloudflare workers, music recognition, edge runtime, wrangler]
---

# Run AudD music recognition in a Cloudflare Worker

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, or
- `POST /` with `multipart/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](https://dashboard.audd.io). The string
  `test` works 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

```bash
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):

```toml
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:

```bash
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.

```ts
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:

```bash
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.

```ts
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:

```bash
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.

```ts
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:

```json
{
  "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:

```ts
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

```bash
wrangler deploy
```

Wrangler prints the deployed URL. Confirm it end to end:

```bash
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 `recognize` on Workers — there
  is no `fs`. Pass a URL string or in-memory bytes (`Blob` / `Uint8Array`).
- **`nodejs_compat` matters.** Without the flag, `FormData` and a few SDK
  internals can behave differently. Keep it on and use a recent
  `compatibility_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
  `limit` while developing.
- **`test` token 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**

- [Build a copyright scanner for user-uploaded content](/resources/recipes/ugc-copyright-scanner)
- [Standard, enterprise, or streams: how to choose](/resources/concepts/standard-vs-enterprise-vs-streams)
- [Node.js SDK docs](https://docs.audd.io/sdks/node)
- [API reference](https://docs.audd.io)