Imagine pulling a source file from S3 and instantly seeing AI‑generated review comments, without a separate CI job. With S3 Object Lambda you can inject Claude (or any LLM) into the read path, turning storage into a live code‑analysis service. This post shows exactly how to make that happen.
Why Instant Code Review Changes the Game
When a teammate opens a file, the most useful feedback is the feedback they can act on right now. Traditional CI pipelines wait until code is pushed, built, and then a separate review step runs. That delay creates three pain points:
- Feedback latency – developers keep working on the same file while the CI job churns in the background.
- Context loss – the moment the review lands in a pull‑request, the developer may have switched tasks, making the comments feel stale.
- Infrastructure churn – a dedicated CI job for code review adds compute time, storage for artifacts, and a separate permission set.
Think of an S3 bucket as a library shelf. In a normal library you walk to the shelf, pull a book, and read it. If you wanted a summary, you’d have to go to a separate desk, request it, wait for the librarian, and then return to the shelf. S3 Object Lambda turns the shelf itself into a smart shelf that hands you the book and a summary in one motion. The “summary” in our case is a JSON file with line‑by‑line suggestions from Claude.
In plain English: By inserting AI into the read operation, the code reviewer becomes part of the storage layer, eliminating the “push‑then‑wait” cycle that most teams accept as normal.
S3 Object Lambda: Transforming Reads on the Fly
S3 Object Lambda is a feature that lets you attach a Lambda function to an access point (a named entry point to a bucket). Every GetObject request that goes through that access point is intercepted, the Lambda runs, and its response is sent back to the caller instead of the raw object.
Key terms:
| Term | Meaning |
|---|---|
| Access point | A named network endpoint that points to a specific bucket (or a subset of it). |
| Object Lambda | The combination of an access point and a Lambda function that can modify the object stream before it’s returned. |
| Invocation policy | An IAM permission (s3-object-lambda:InvokeFunction) that lets the access point call your Lambda. |
A common surprise is that the access point does not inherit the bucket’s IAM policies. You must give the Lambda both permission to be invoked and permission to read the original object (s3:GetObject). Forgetting either results in a AccessDenied error when the caller tries to read the file.
Minimal setup code (Node.js)
// src/createAccessPoint.ts
import {
S3Client,
CreateAccessPointCommand,
} from "@aws-sdk/client-s3";
const s3 = new S3Client({ region: "us-east-1" });
async function createObjectLambdaAP() {
// Create an Object Lambda Access Point that points at the source bucket.
// The ARN below must be replaced with your bucket's ARN.
const command = new CreateAccessPointCommand({
AccountId: "123456789012", // your AWS account ID
Name: "code-review-ap", // friendly name for the AP
ObjectLambdaConfiguration: {
SupportingAccessPoint: "arn:aws:s3:us-east-1:123456789012:accesspoint/source-bucket-ap",
// The Lambda that will transform the object.
CloudWatchMetricsEnabled: true,
// The IAM role that gives the AP permission to invoke the Lambda.
// This role must have s3-object-lambda:InvokeFunction on the Lambda ARN.
// and s3:GetObject on the source bucket.
// See the IAM section later for the exact policy.
},
});
const response = await s3.send(command);
console.log("Created Object Lambda AP:", response.AccessPointArn);
}
createObjectLambdaAP().catch(console.error);
Tip: When you create the supporting access point (
source-bucket-ap), give it the usuals3:GetObjectpermission. The Object Lambda AP has its own policy—don’t assume the bucket’s policy will apply.
Calling Claude Inside the Object Lambda Handler
The Lambda you attach to the access point receives the original object as a stream (Node.js Readable). To keep memory usage low, we pipe that stream into the body of an HTTP request that talks to Claude’s API. Claude then returns a JSON payload with suggestions. Finally we return two objects to the caller:
- The original file (unchanged) – so existing tools keep working.
- A side‑car object named
<filename>.review.json– containing the AI review.
Because the Lambda must return a single response, we pack both pieces into a multipart/related payload. Most HTTP clients understand this pattern, and browsers can download the original file while the side‑car can be fetched programmatically.
Complete Lambda handler (Node.js 22)
// src/objectLambdaHandler.ts
import {
S3ObjectLambdaClient,
GetObjectCommand,
} from "@aws-sdk/client-s3-object-lambda";
import { Readable } from "stream";
// The Claude endpoint – replace with your actual URL and key.
const CLAUDE_ENDPOINT = "https://api.anthropic.com/v1/complete";
const CLAUDE_API_KEY = process.env.CLAUDE_API_KEY!;
// Helper: read a stream into a string (small files only, safe for source files).
async function streamToString(stream: Readable): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of stream) {
chunks.push(Buffer.from(chunk));
}
return Buffer.concat(chunks).toString("utf-8");
}
// The Lambda entry point required by Object Lambda.
export const handler = async (event: any) => {
// 1️⃣ Extract the key (filename) from the incoming request.
const key = event.getObjectContext?.inputS3Url?.split("/").pop();
if (!key) {
throw new Error("Could not determine object key");
}
// 2️⃣ Create a client that can read the original object.
const s3 = new S3ObjectLambdaClient({ region: "us-east-1" });
// 3️⃣ Pull the raw file from the backing bucket.
const getCmd = new GetObjectCommand({ Bucket: event.bucket, Key: key });
const { Body } = await s3.send(getCmd);
if (!Body) throw new Error("Empty object body");
// 4️⃣ Convert the stream to text – source files are usually < 1 MiB.
const sourceCode = await streamToString(Body as Readable);
// 5️⃣ Ask Claude for a review. The prompt asks for line‑by‑line JSON.
const reviewResponse = await fetch(CLAUDE_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": CLAUDE_API_KEY,
},
body: JSON.stringify({
model: "claude-3-5-sonnet-20240620",
prompt: `You are a friendly code reviewer. Return a JSON array where each element has:
- line: line number (1‑based)
- suggestion: short improvement comment
Only output JSON, no prose.
File content follows:\n${sourceCode}`,
max_tokens: 1024,
}),
});
const reviewJson = await reviewResponse.json();
// 6️⃣ Build a multipart response: original file + .review.json side‑car.
const boundary = "----AIReviewBoundary";
const multipartBody = [
`--${boundary}`,
"Content-Type: application/octet-stream",
`Content-Disposition: attachment; filename="${key}"`,
"",
sourceCode,
`--${boundary}`,
"Content-Type: application/json",
`Content-Disposition: attachment; filename="${key}.review.json"`,
"",
JSON.stringify(reviewJson, null, 2),
`--${boundary}--`,
"",
].join("\r\n");
// 7️⃣ Return the transformed object back to the caller.
return {
statusCode: 200,
// Object Lambda expects the body to be a Uint8Array or string.
body: multipartBody,
// Important: tell the client we are sending multipart data.
headers: {
"Content-Type": `multipart/related; boundary=${boundary}`,
},
};
};
What’s happening line‑by‑line?
- Step 1 pulls the filename from the request URL.
- Step 3‑4 fetches the raw source from the backing bucket and turns it into a plain string.
- Step 5 sends that string to Claude with a prompt that asks for a JSON‑only review.
- Step 6 builds a multipart payload so the caller receives both the original file and the review side‑car in one HTTP response.
-
Step 7 returns the payload with the correct
Content‑Typeheader.
Key takeaway: The Lambda does not need to rewrite the original file; it simply adds a review alongside it, keeping the storage contract unchanged for any existing tooling.
Building the Node.js 22 Service that Ties It All Together
Now that the Lambda does the heavy lifting, we need a small service that:
-
Creates the required IAM roles and policies (including the missing
s3:GetObject). -
Deploys the Lambda with the correct runtime (
nodejs22.x). - Registers the Object Lambda access point pointing at the bucket that holds source files.
-
Provides a simple client that developers can call (
fetcha signed URL) and instantly receive the review.
1️⃣ IAM policy for the Object Lambda AP
// src/iamPolicy.ts
import {
IAMClient,
CreatePolicyCommand,
AttachRolePolicyCommand,
} from "@aws-sdk/client-iam";
const iam = new IAMClient({ region: "us-east-1" });
async function createPolicy() {
// The policy gives the Object Lambda AP permission to:
// • Invoke the Lambda (s3-object-lambda:InvokeFunction)
// • Read objects from the source bucket (s3:GetObject)
const policyDocument = {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: "s3-object-lambda:InvokeFunction",
Resource: "arn:aws:lambda:us-east-1:123456789012:function:code-review-lambda",
},
{
Effect: "Allow",
Action: "s3:GetObject",
Resource: "arn:aws:s3:::my-code-bucket/*",
},
],
};
const createCmd = new CreatePolicyCommand({
PolicyName: "ObjectLambdaReviewPolicy",
PolicyDocument: JSON.stringify(policyDocument),
});
const { Policy } = await iam.send(createCmd);
console.log("Policy ARN:", Policy?.Arn);
// Attach to the role that backs the access point.
const attachCmd = new AttachRolePolicyCommand({
RoleName: "ObjectLambdaAPRole",
PolicyArn: Policy!.Arn,
});
await iam.send(attachCmd);
console.log("Policy attached to role.");
}
createPolicy().catch(console.error);
Tip: Double‑check that the
ResourceARN for the Lambda matches the qualified version (e.g.,function:my-func:$LATEST) if you use versioning.
2️⃣ Deploy the Lambda (using AWS SDK, not SAM)
// src/deployLambda.ts
import {
LambdaClient,
CreateFunctionCommand,
AddPermissionCommand,
} from "@aws-sdk/client-lambda";
import { readFileSync } from "fs";
import { join } from "path";
const lambda = new LambdaClient({ region: "us-east-1" });
async function deploy() {
const zipBuffer = readFileSync(join(__dirname, "code-review-lambda.zip")); // pre‑zipped handler
// Create the function with Node.js 22 runtime.
const createCmd = new CreateFunctionCommand({
FunctionName: "code-review-lambda",
Runtime: "nodejs22.x",
Role: "arn:aws:iam::123456789012:role/CodeReviewLambdaRole",
Handler: "objectLambdaHandler.handler",
Code: { ZipFile: zipBuffer },
// SnapStart can speed up cold starts, but note the VPC caveat later.
SnapStart: { ApplyOn: "PublishedVersions" },
});
const { FunctionArn } = await lambda.send(createCmd);
console.log("Created Lambda:", FunctionArn);
// Grant the Object Lambda AP permission to invoke this function.
const permCmd = new AddPermissionCommand({
FunctionName: "code-review-lambda",
StatementId: "AllowObjectLambdaInvoke",
Action: "lambda:InvokeFunction",
Principal: "s3-object-lambda.amazonaws.com",
// The source ARN is the AP we created earlier.
SourceArn: "arn:aws:s3-object-lambda:us-east-1:123456789012:accesspoint/code-review-ap",
});
await lambda.send(permCmd);
console.log("Invocation permission added.");
}
deploy().catch(console.error);
Why we use the raw SDK instead of a higher‑level tool: The SDK lets us see each required permission and the exact shape of the request, which is valuable when troubleshooting the “AccessDenied” gotcha.
3️⃣ Simple client that fetches a file and reads the review
// src/fetchWithReview.ts
import {
S3Client,
GetObjectCommand,
} from "@aws-sdk/client-s3";
import { createPresignedUrl } from "@aws-sdk/s3-request-presigner";
import { Readable } from "stream";
const s3 = new S3Client({ region: "us-east-1" });
async function getFileWithReview(bucket: string, key: string) {
// Generate a presigned URL that goes through the Object Lambda AP.
const getCmd = new GetObjectCommand({
Bucket: `${bucket}.code-review-ap`, // access point name appended with bucket
Key: key,
});
const url = await createPresignedUrl(s3, getCmd, { expiresIn: 300 });
// Fetch the multipart response.
const response = await fetch(url);
const contentType = response.headers.get("content-type")!;
const boundary = contentType.split("boundary=")[1];
const rawBody = await response.text();
// Very naive multipart parser – just for demo.
const parts = rawBody.split(`--${boundary}`).filter(p => p.trim());
const original = parts[0].split("\r\n\r\n")[1];
const reviewJson = parts[1].split("\r\n\r\n")[1];
console.log("Original file:\n", original);
console.log("AI Review:\n", JSON.parse(reviewJson));
}
// Example usage
getFileWithReview("my-code-bucket", "utils/helpers.ts").catch(console.error);
In plain English: The client asks S3 for
utils/helpers.tsthrough the Object Lambda access point. S3 hands the request to our Lambda, which tacks on a review file, and the client receives both pieces in a single HTTP call.
4️⃣ Quick sanity check: what if the policy is missing?
If you run the client and see AccessDenied with a message like User: arn:aws:sts::... is not authorized to perform: s3-object-lambda:InvokeFunction, double‑check:
- The policy attached to the access point role includes
s3-object-lambda:InvokeFunction. - The same role also has
s3:GetObjecton the source bucket.
Missing either line causes the exact error many teams hit first.
Observability, Error Handling, and Real‑World Gotchas
Logging and metrics
Object Lambda automatically streams logs to CloudWatch, but you’ll want structured logs (JSON) so you can query for:
- Latency – time spent reading the object vs. time spent calling Claude.
- Error rate – network failures to Claude, JSON parsing errors, or malformed multipart payloads.
// src/objectLambdaHandler.ts (excerpt)
import { createLogger } from "pino";
const logger = createLogger({ level: "info" });
export const handler = async (event: any) => {
const start = Date.now();
try {
// ... existing logic ...
const duration = Date.now() - start;
logger.info({
key,
durationMs: duration,
reviewItems: reviewJson.length,
});
return { /* response */ };
} catch (err) {
logger.error({ err, key }, "Failed to process Object Lambda request");
// Return a 500 so the caller knows something went wrong.
return {
statusCode: 500,
body: "Internal Server Error",
};
}
};
Handling large files
The demo reads the whole file into memory (streamToString). For files > 5 MiB you should:
- Stream the request to Claude (Claude’s API supports streaming bodies).
- Chunk the review – send the file in sections and merge the JSON pieces.
If you forget, the Lambda may hit the memory limit and crash, which surfaces as a 502 from S3.
SnapStart + VPC gotcha
Many teams enable SnapStart expecting instant cold starts. However, if the Lambda sits inside a VPC (common when you need access to private VPC resources), SnapStart has no effect because the cold‑start time is dominated by VPC ENI attachment, not the function code. Either keep the Lambda outside the VPC (the Claude API is public) or accept the extra cost.
Tip: Keep the Lambda lightweight, no database connections, no VPC, and you’ll see sub‑100 ms cold starts even without SnapStart.
Provisioned Concurrency surprise
If you enable Provisioned Concurrency to guarantee capacity, you’ll see a steady charge even when nobody is reading code. For an on‑demand review service, it’s usually cheaper to rely on the natural burst capacity of Lambda.
Transfer acceleration and presigned URLs
When you generate a presigned URL that points at the Object Lambda AP, Transfer Acceleration does not apply. Trying to add the accelerate flag to the client will cause a 400 error. Keep the URL plain unless you have a separate CloudFront distribution in front of the AP.
The Takeaway
What you now have: a complete, minimal stack that turns an S3 read into an instant AI‑powered code review.
- Instant feedback moves the review step from “after push” to “when you open the file”.
- S3 Object Lambda acts like a smart library shelf, attaching a review side‑car to every read request.
-
IAM is explicit: you must give the access point both
s3-object-lambda:InvokeFunctionands3:GetObject; otherwise reads fail withAccessDenied. -
The Lambda handler streams the original file, calls Claude via
fetch, builds a multipart response, and returns it—all in under a second for typical source files. - Observability matters: log duration, errors, and review size; watch out for memory limits on larger files.
- Common AWS gotchas (SnapStart + VPC, Provisioned Concurrency costs, Transfer Acceleration limits) can be avoided by keeping the Lambda simple and public‑facing.
Give it a try on a small repository, measure the latency, and you’ll see how a few lines of code can make the whole development loop feel a lot tighter. Happy reviewing!
Transparency notice
This article was written with the help of an AI system — Groq (GPT OSS 120B).
Published: 2026-08-20 · Primary focus: S3
All code blocks are intended to be correct and runnable, but please verify them
against the official docs for the tools mentioned before using in production.Find an error? Drop a comment — corrections are always welcome.
Top comments (0)