Run AudD music recognition in an AWS Lambda function
Build an AWS Lambda function in Node.js that identifies music with AudD, triggered by API Gateway or by new objects landing in S3.
This page shows how to run AudD music recognition inside an AWS Lambda function on the Node.js runtime. You’ll build two trigger shapes: an API Gateway HTTP endpoint that recognizes a clip on demand, and an S3 trigger that scans audio objects as they’re uploaded. It’s for anyone who wants recognition without standing up a long-running server.
What you’ll build
A single Lambda function package that wraps the @audd/sdk client. The
handler reads its input — either an HTTP request from API Gateway or an S3
event record — gets the audio in front of the SDK, calls AudD, and returns
the match. The API token lives in the function’s configuration, not in the
code. The two triggers share the same recognition core; only the way the
audio arrives differs.
The standard endpoint handles short clips and returns under two seconds, which fits comfortably inside a default Lambda timeout. The enterprise endpoint handles long files and chunks them server-side over minutes — long enough that you have to think about the Lambda timeout, which the last section covers.
Prerequisites
- An API token from dashboard.audd.io.
- An AWS account with permission to create Lambda functions, and either an API Gateway HTTP API or an S3 bucket depending on the trigger you want.
- Node.js 20 or newer locally for packaging. The function targets the
nodejs20.x(or newer) Lambda runtime, which is what@audd/sdkrequires. - The AWS CLI configured, or the console — the steps below use the CLI.
Walkthrough
Step 1: Package the dependency
Lambda doesn’t install dependencies for you. @audd/sdk and its transitive
modules have to be in the deployment artifact. Create a project, install the
SDK, and you’ll zip node_modules together with your handler.
mkdir audd-lambda && cd audd-lambda
npm init -y
npm install @audd/sdk
The SDK is dual ESM + CJS and ships its own TypeScript types. If your
function uses ESM, set "type": "module" in package.json; the examples
below use ESM import syntax. Keep the artifact small — the SDK has a light
dependency tree, so a plain zip of the project directory is enough. For
anything more elaborate, a Lambda layer or a bundler like esbuild works too,
but isn’t required here.
Step 2: Store the API token in the function configuration
Never hard-code the token in the handler. The simplest option is a Lambda environment variable, set on the function and read at runtime:
aws lambda update-function-configuration \
--function-name audd-recognize \
--environment "Variables={AUDD_API_TOKEN=your-api-token}"
The SDK reads AUDD_API_TOKEN automatically when you construct the client
with no argument, so the handler stays clean:
import { AudD } from "@audd/sdk";
// AUDD_API_TOKEN is read from the Lambda environment.
const audd = new AudD();
Prefer Secrets Manager for production tokens. A Lambda environment variable is fine for a quick deploy, but it’s visible to anyone with
GetFunctionConfigurationand is stored at rest with the function. For production, put the token in AWS Secrets Manager, grant the function’s execution rolesecretsmanager:GetSecretValue, fetch it once on cold start, and pass it to the constructor. See Step 6.
Construct the client outside the handler function so it’s reused across warm invocations rather than rebuilt on every request.
Step 3: Recognize a clip behind API Gateway
The first trigger is an HTTP API: a client POSTs either a JSON body with a URL to recognize, or raw audio bytes, and gets the match back as JSON. This uses the standard endpoint — fast, with a 10 MB file-size cap.
import { AudD } from "@audd/sdk";
const audd = new AudD();
export const handler = async (event) => {
const contentType = event.headers?.["content-type"] ?? "";
let source;
if (contentType.includes("application/json")) {
// { "url": "https://audd.tech/example.mp3" }
const body = JSON.parse(event.body ?? "{}");
source = body.url;
} else {
// Raw audio bytes. API Gateway base64-encodes binary bodies.
source = event.isBase64Encoded
? Buffer.from(event.body, "base64")
: Buffer.from(event.body);
}
const song = await audd.recognize(source, {
returnMetadata: ["apple_music", "spotify"],
});
if (!song) {
return { statusCode: 200, body: JSON.stringify({ match: null }) };
}
return {
statusCode: 200,
body: JSON.stringify({
match: {
artist: song.artist,
title: song.title,
album: song.album,
songLink: song.songLink,
timecode: song.timecode,
appleMusic: song.streamingUrl("apple_music"),
spotify: song.streamingUrl("spotify"),
},
}),
};
};
recognize returns null on a successful call that found no match; that’s
distinct from an error, so map it to a 200 with match: null, not a 404.
API Gateway needs binary media types configured for raw audio. If you POST bytes rather than a URL, add
audio/*(or the specific MIME type) to the API’s binary media types, or API Gateway will mangle the body. The URL-based path avoids this entirely — the SDK passes the URL to AudD and AudD fetches the audio server-side.
Test the deployed endpoint with the always-available example file:
curl -X POST "https://<api-id>.execute-api.<region>.amazonaws.com/recognize" \
-H "content-type: application/json" \
-d '{"url":"https://audd.tech/example.mp3"}'
You’ll get back the artist, title, and streaming links for the example track.
Step 4: Scan audio dropped into S3
The second trigger fires when an object lands in a bucket. Lambda passes an S3 event with the bucket and key; the handler downloads the object’s bytes and recognizes them. This is the pattern for “anything uploaded here gets identified.”
import { AudD } from "@audd/sdk";
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
const audd = new AudD();
const s3 = new S3Client({});
export const handler = async (event) => {
for (const record of event.Records) {
const bucket = record.s3.bucket.name;
const key = decodeURIComponent(
record.s3.object.key.replace(/\+/g, " "),
);
const obj = await s3.send(
new GetObjectCommand({ Bucket: bucket, Key: key }),
);
const bytes = Buffer.from(await obj.Body.transformToByteArray());
const song = await audd.recognize(bytes);
if (song) {
console.log(`${key}: ${song.artist} — ${song.title}`);
} else {
console.log(`${key}: no match`);
}
}
};
The @aws-sdk/client-s3 module is available in the Lambda Node.js runtime,
so you don’t have to bundle it — only @audd/sdk. Grant the execution role
s3:GetObject on the source bucket.
For short clips this is complete as written: the bytes go straight from S3
into recognize with no disk involved. If you want to write each verdict
somewhere, push it to DynamoDB, an SQS queue, or back into S3 as a sidecar
JSON object — the recognition call is the only AudD-specific part.
Don’t write the Lambda’s output back into the same bucket prefix that triggers it. If the trigger watches the whole bucket and your handler writes a result object into it, you create a trigger loop. Scope the trigger to a prefix (for example
uploads/) and write results to a different prefix or bucket.
Step 5: Stage a large object in /tmp when you need a file path
The SDK accepts bytes directly, so most handlers never touch disk. But
Lambda gives each function 512 MB of writable scratch in /tmp (more if you
configure ephemeral storage), which is useful when an object is large enough
that you’d rather stream it to disk than hold it all in memory, or when a
downstream step wants a real file path.
import { createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import { unlink } from "node:fs/promises";
const tmpPath = `/tmp/${key.split("/").pop()}`;
await pipeline(obj.Body, createWriteStream(tmpPath));
try {
const song = await audd.recognize(tmpPath);
// ...
} finally {
await unlink(tmpPath).catch(() => {});
}
Clean up /tmp in a finally block. The scratch space persists across warm
invocations of the same execution environment, so a function that doesn’t
clean up can fill it and fail on a later call.
Step 6: Pull the token from Secrets Manager (production)
Instead of an environment variable, fetch the token once on cold start and pass it to the constructor. Cache it in module scope so warm invocations reuse it.
import { AudD } from "@audd/sdk";
import {
SecretsManagerClient,
GetSecretValueCommand,
} from "@aws-sdk/client-secrets-manager";
const sm = new SecretsManagerClient({});
let auddPromise;
function getClient() {
if (!auddPromise) {
auddPromise = sm
.send(new GetSecretValueCommand({ SecretId: "audd/api-token" }))
.then((res) => new AudD(res.SecretString));
}
return auddPromise;
}
export const handler = async (event) => {
const audd = await getClient();
const song = await audd.recognize("https://audd.tech/example.mp3");
// ...
};
Grant the execution role secretsmanager:GetSecretValue on that secret’s
ARN. If you rotate the token, audd.setApiToken(next) updates the client
without rebuilding it — in-flight requests finish on the old token.
Timeouts: standard vs. enterprise scans
The default Lambda timeout is short (3 seconds unless you raise it). Match the timeout to the endpoint you call.
- Standard recognition returns under two seconds. A 10-second Lambda timeout is plenty. Both the API Gateway and S3 examples above use the standard endpoint and are safe at the default once you bump it slightly.
- Enterprise scans of long files chunk server-side and can run for
minutes. The SDK’s default per-call timeout for
recognizeEnterpriseis one hour. A synchronous Lambda invocation can’t wait that long — API Gateway caps the integration at 29 seconds, and Lambda’s own maximum is 15 minutes.
If you need enterprise recognition on long files, don’t run it synchronously behind API Gateway. Use one of these instead:
- Async / event-driven. Trigger the Lambda from S3 or an SQS queue (not
a synchronous HTTP request), set the function timeout to cover your typical
file, and write the result somewhere the caller polls. Always set
limiton the call — enterprise bills per 12 seconds of audio processed, and an unbounded scan of a long file can ingest hours of audio. - Step Functions. For files that exceed even the 15-minute Lambda
ceiling, or when you want retries and visibility, model the scan as a Step
Functions state machine. Pass
opts.signal(anAbortSignal) so the function can cancel the call if the state machine times out.
// Enterprise scan in an async (S3- or SQS-triggered) Lambda.
const matches = await audd.recognizeEnterprise(bytes, {
limit: 25, // always cap during development and in async jobs
timeoutMs: 600_000, // 10 min, under the 15-min Lambda ceiling
});
for (const m of matches) {
console.log(m.timecode, m.artist, "—", m.title);
}
Always set
limiton enterprise calls. The enterprise endpoint bills per 12 seconds of audio processed. An unbounded call on a long upload can meter hours of audio. Start withlimit=25and raise it once you understand the cost on your real inputs.
Handling errors
The SDK raises typed errors. In a Lambda, the categories that matter:
- Authentication errors — bad or missing token. With the env-var or Secrets Manager setup above this surfaces on the first call after a bad deploy. Let it fail the invocation so it shows in CloudWatch; don’t retry.
- Quota / subscription errors — you’ve hit a request limit, or the enterprise endpoint isn’t enabled on your token. Surface to ops. Don’t retry in a loop, which burns invocations.
- Invalid-audio errors — the input wasn’t decodable audio. For the API
Gateway trigger, return a
422. For the S3 trigger, log it and move on; don’t let one bad object fail the whole batch. - Connection errors — transient. Safe to retry with backoff. For S3/SQS triggers, letting the invocation fail lets Lambda’s built-in retry and dead-letter handling take over.
try {
const song = await audd.recognize(source);
return { statusCode: 200, body: JSON.stringify({ match: song ?? null }) };
} catch (err) {
// err.errorCode / err.requestId are useful for support tickets.
console.error("recognition failed", err);
return { statusCode: 502, body: JSON.stringify({ error: "scan_failed" }) };
}
Going further
- Run the same recognition core at the edge instead of in a region — see the Cloudflare Workers integration.
- To turn the S3 trigger into a full moderation pipeline that captures ISRC, UPC, and label for every match, see Build a copyright scanner for user-uploaded content.
- Keep enterprise cost bounded with
limit,every, andskip— see Enterprise cost optimization.
Related
Reading this as an AI agent? The raw Markdown is at integrations/aws-lambda.md, and the full index is /resources/llms.txt.
