DEV Community

Dinesh_gowtham
Dinesh_gowtham

Posted on

S3 Object Lambda for AI: Transforming Images on the Fly for Claude Vision, Explained Simply

Developers often copy images from S3 to a server just to resize them before sending them to a LLM’s vision model. What if the transformation could happen inside S3, eliminating the extra hop? This post shows how to let Claude’s Vision API see resized images without any custom compute.

Why Object Lambda matters for AI workloads

When an AI service asks for an image, the usual pattern looks like this:

  1. A backend pulls the original file from an S3 bucket.
  2. The backend runs a resizing library (Sharp, Pillow, etc.).
  3. The resized bytes travel over the network again, this time to the AI endpoint.

Each step adds latency, CPU usage, and a point of failure. Object Lambda changes the story by letting you attach a tiny piece of code (a transform function) directly to an S3 access point. The function runs inside the S3 service whenever a GET request arrives, so the caller receives the transformed data immediately.

In plain English: Think of a regular S3 bucket as a pantry that only stores food. Object Lambda turns the pantry into a kitchen that can chop, slice, or bake items the moment you ask for them—no extra chef needed.

For vision models like Claude Vision (Anthropic’s multimodal LLM), the model cares only about the final pixel dimensions, not how you got there. By moving resizing into S3:

  • Latency drops – one network round‑trip instead of two.
  • Compute cost falls – you no longer need an EC2 or Lambda worker just for image prep.
  • Security improves – the original high‑resolution file never leaves the bucket, only the trimmed version.

A quick term list

  • Object Lambda – a feature that lets you run a Lambda function during an S3 GET/HEAD request, returning the transformed result to the caller.
  • Access point – a named entry point for a bucket that can have its own policy and can be hooked to an Object Lambda function.
  • LLM (Large Language Model) – a neural network that can understand or generate text; when it also processes images it’s called a vision model.

Tip: Object Lambda works only with GET and HEAD operations. It cannot write the transformed image back to S3; the result lives only in the response stream.

Setting up an S3 Object Lambda access point

Before we can call the access point from code, we must create three pieces in the AWS console (or via CloudFormation/CDK). The steps below use the AWS CLI for clarity, but the same API calls exist in the @aws-sdk/client-s3 JavaScript package.

1. Write the transform function

The function receives the original object, resizes it to 256 × 256, and streams the result back. For simplicity we’ll use the sharp library, which works in Lambda’s Node.js runtime.

// lambda-resize.ts
import { S3ObjectLambdaEvent, S3ObjectLambdaResponse } from 'aws-lambda';
import sharp from 'sharp';

// Lambda entry point
export const handler = async (event: S3ObjectLambdaEvent): Promise<S3ObjectLambdaResponse> => {
  // The original object is streamed from S3 for us.
  const originalStream = event.getObjectContext?.inputStream;
  if (!originalStream) {
    throw new Error('No input stream – something went wrong.');
  }

  // Transform: resize to 256×256 and force JPEG output.
  const resizedStream = originalStream.pipe(
    sharp()
      .resize(256, 256)          // <-- desired dimensions
      .jpeg({ quality: 80 })     // <-- keep size reasonable
  );

  // Return the transformed stream back to the caller.
  return {
    getObjectResponse: {
      statusCode: 200,
      body: resizedStream,
      // Must set a proper Content-Type so Claude knows it’s a JPEG.
      header: { 'Content-Type': 'image/jpeg' },
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

Key takeaway: The Lambda never writes anything to S3; it simply reads the incoming stream, reshapes it, and sends the new bytes straight back.

2. Deploy the Lambda

# Build the TypeScript bundle (skip if you prefer plain JavaScript)
npm run build

# Create the Lambda function
aws lambda create-function \
  --function-name ResizeForClaude \
  --runtime nodejs22.x \
  --handler lambda-resize.handler \
  --zip-file fileb://dist/resizer.zip \
  --role arn:aws:iam::123456789012:role/LambdaS3ObjectLambdaRole \
  --memory-size 256 \
  --timeout 10
Enter fullscreen mode Exit fullscreen mode

Tip: Give the role AWSLambdaS3ObjectLambdaExecutionRolePolicy managed policy. It includes the minimal s3-object-lambda:* permissions.

3. Create an Object Lambda access point

# First, a regular S3 access point for the bucket (replace placeholders)
aws s3control create-access-point \
  --account-id 123456789012 \
  --name images-ap \
  --bucket my-image-bucket

# Then, an Object Lambda access point that ties the Lambda to the above.
aws s3control create-access-point-for-object-lambda \
  --account-id 123456789012 \
  --name resize-ap \
  --configuration '{
    "SupportingAccessPoint": "arn:aws:s3:us-east-1:123456789012:accesspoint/images-ap",
    "TransformationConfigurations": [{
      "Actions": ["GetObject","HeadObject"],
      "ContentTransformation": {
        "AwsLambda": {
          "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:ResizeForClaude"
        }
      }
    }]
  }'
Enter fullscreen mode Exit fullscreen mode

In plain English: The resize-ap access point is like a door that says “whenever someone asks for a file, run ResizeForClaude first, then hand the result out”.

Permissions you must not forget

Object Lambda needs read access to the original bucket and the access point you just made. If either policy is missing, you’ll hit an AccessDenied error at runtime.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::my-image-bucket",
        "arn:aws:s3:::my-image-bucket/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": "s3-object-lambda:GetObject",
      "Resource": "arn:aws:s3-object-lambda:us-east-1:123456789012:accesspoint/resize-ap"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Gotcha: Object Lambda only supports GET and HEAD. If you later try a PUT through the same access point, AWS will reject it with AccessDenied.

Calling the access point from Node.js 22

Now that the infrastructure is ready, the client side can treat the access point like any other S3 endpoint. The only twist is that the endpoint URL contains the access point name instead of the bucket name.

Install the SDK

npm i @aws-sdk/client-s3 node-fetch
Enter fullscreen mode Exit fullscreen mode

Full script with comments

// fetch-resized.js
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import fetch from 'node-fetch';
import { pipeline } from 'stream';
import { promisify } from 'util';

// ---------------------------------------------------------------------
// 1️⃣  Prepare the S3 client that talks to the Object Lambda access point.
//    The endpoint format is:
//    https://<access-point-name>-<account-id>.s3-object-lambda.<region>.amazonaws.com
// ---------------------------------------------------------------------
const REGION = 'us-east-1';
const ACCESS_POINT = 'resize-ap';               // <-- name from setup step
const ACCOUNT_ID = '123456789012';
const ENDPOINT = `https://${ACCESS_POINT}-${ACCOUNT_ID}.s3-object-lambda.${REGION}.amazonaws.com`;

const s3 = new S3Client({
  region: REGION,
  endpoint: ENDPOINT,           // Directs all calls to the Object Lambda AP
  forcePathStyle: false,        // Needed for the Object Lambda hostname style
});

// ---------------------------------------------------------------------
// 2️⃣  Define the object we want to fetch (original key in the bucket).
// ---------------------------------------------------------------------
const BUCKET = 'my-image-bucket'; // Not used in the request, but required by SDK signature
const KEY = 'photos/large/photo123.jpg';

// ---------------------------------------------------------------------
// 3️⃣  GetObject returns a stream. We'll pipe it straight into Claude.
// ---------------------------------------------------------------------
async function sendToClaude() {
  try {
    const getCmd = new GetObjectCommand({ Bucket: BUCKET, Key: KEY });
    const response = await s3.send(getCmd);

    // The response.Body is a Node.js readable stream containing the resized JPEG.
    const imageStream = response.Body;

    // -----------------------------------------------------------------
    // 4️⃣  Call Claude Vision. The API expects a multipart/form-data body
    //    with the image under the "image" field. We'll use fetch and a
    //    FormData polyfill (node-fetch supports it natively from v3+).
    // -----------------------------------------------------------------
    const form = new FormData();
    form.append('image', imageStream, {
      filename: 'photo_resized.jpg',
      contentType: 'image/jpeg', // Must match what Object Lambda set
    });

    const claudeResp = await fetch('https://api.anthropic.com/v1/vision', {
      method: 'POST',
      headers: {
        'x-api-key': process.env.ANTHROPIC_API_KEY,
        // fetch automatically adds the multipart boundary header
      },
      body: form,
    });

    if (!claudeResp.ok) {
      throw new Error(`Claude returned ${claudeResp.status}`);
    }

    const result = await claudeResp.json();
    console.log('Claude response:', result);
  } catch (err) {
    // ---------------------------------------------------------------
    // 5️⃣  Special handling for AccessDenied – most likely a policy
    //    mis‑configuration on the access point or the Lambda role.
    // ---------------------------------------------------------------
    if (err.name === 'AccessDenied' || (err.$metadata && err.$metadata.httpStatusCode === 403)) {
      console.error('❌ AccessDenied: check that the access point has s3:ListBucket and s3:GetObject on both the bucket and the AP.');
    } else {
      console.error('❌ Unexpected error:', err);
    }
  }
}

// Run the function
sendToClaude();
Enter fullscreen mode Exit fullscreen mode

Helpful tip: When you see a 403 from the GetObjectCommand, first double‑check the bucket policy, the access point policy, and the Lambda execution role. All three need the same read permissions.

Common S3 SDK gotchas that bite beginners

Gotcha Why it matters How to avoid
S3 Express One Zone uses a different set of API operations (CreateObject vs PutObject). If you switch to this storage class, your existing @aws-sdk/client-s3 v2 code may start throwing InvalidRequest. Stick with Standard unless you need the cost trade‑off, and test with the same SDK version.
Presigned URLs expire after the default 15 minutes. Teams often generate a URL, hand it to a front‑end, and then get 403 when the user clicks later. Explicitly set expiresIn to a value that covers the longest expected wait, and handle the 403 gracefully (refresh the URL).
Object Lock in Compliance mode cannot be deleted, even by root. Accidentally enabling it on a bucket prevents any delete or overwrite, leading to storage bloat. Enable Object Lock only on buckets that truly need immutable records, and choose Governance mode if you still need occasional deletions.
List operations are eventually consistent. A newly uploaded image might not appear immediately in a ListObjectsV2 call, causing “file not found” errors in pipelines. Use S3 event notifications or a short retry loop after upload.
Transfer acceleration adds extra cost and requires a separate endpoint. Teams enable it for faster uploads but forget the extra $ per GB, inflating budgets. Enable only when you truly need cross‑continent speed, and monitor the cost report.

Feeding the transformed image to Claude Vision

Claude Vision expects a multipart request where the image part has a correct MIME type (image/jpeg, image/png, etc.). Because our Object Lambda already sets Content-Type: image/jpeg, we simply forward the stream.

Why streaming matters

If we first buffered the entire image into memory, we would lose the memory‑efficiency benefit of Object Lambda (which streams data directly from S3). Streaming also reduces latency because Claude can start reading the bytes while the resize is still happening.

Minimal fetch code recap

const form = new FormData();
form.append('image', imageStream, {
  filename: 'photo_resized.jpg',
  contentType: 'image/jpeg',
});

await fetch('https://api.anthropic.com/v1/vision', {
  method: 'POST',
  headers: { 'x-api-key': process.env.ANTHROPIC_API_KEY },
  body: form,
});
Enter fullscreen mode Exit fullscreen mode

In plain English: Think of the FormData object as a courier bag. You place the freshly sliced image inside, seal it with the correct label (Content-Type), and hand the bag to the courier (the fetch call). The courier delivers it straight to Claude without ever opening the bag.

Handling Claude’s response

Claude returns a JSON payload that contains either a textual description or a structured result, depending on the prompt you sent. The example script logs the whole response; in production you would parse the fields you need.

Performance & cost considerations

Latency

Step Traditional flow Object Lambda flow
Network hop 1 S3 → EC2 (or Lambda) S3 → Object Lambda (same region)
Compute Resize on EC2/Lambda Resize inside S3 service
Network hop 2 EC2/Lambda → Claude S3 → Claude (via your app)
Total 2‑3× higher latency ~1× lower latency

Because Object Lambda runs in the same AWS edge location that serves the GET request, the extra processing time is typically under 30 ms for a 256 × 256 JPEG.

Cost

Component Approximate cost (per 1 M requests)
S3 GET request $0.0004
Object Lambda invocation $0.0005 (per 1 K invocations) → $0.50
Data transfer (out to internet) $0.09 per GB
Claude Vision API $0.015 per 1 000 images (example)

Overall, you pay a few dollars per million image requests, far cheaper than running a dedicated Lambda just for resizing (which would add compute minutes and additional memory charges).

Tip: Keep an eye on the “Object Lambda data processed” metric in CloudWatch; it tells you how many bytes the service actually reshaped, useful for estimating future cost.

Scaling

Object Lambda scales automatically with the underlying S3 service. There’s no need to provision concurrency or worry about cold starts—AWS launches the transformation function on demand, reusing containers when possible.

When not to use Object Lambda

  • Write‑back needed – If you want the resized image stored for later reuse, Object Lambda alone isn’t enough; you’d need a follow‑up Lambda that writes the result to another bucket.
  • Complex pipelines – Multi‑step processing (e.g., watermark → resize → format conversion) may exceed the 15 second timeout limit for Object Lambda functions. In such cases chain regular Lambdas instead.

The Takeaway

What you should remember after reading this guide:

  • Object Lambda lets you attach a tiny transformation function to an S3 access point, turning a passive store into an on‑demand data‑shaping layer.
  • For vision models like Claude, resizing images inside S3 removes a whole server hop, cutting latency and compute cost.
  • The access point must have s3:ListBucket and s3:GetObject permissions on both the original bucket and the access point itself; otherwise you’ll hit AccessDenied.
  • Only GET and HEAD are supported; you can’t write the transformed file back to S3 from the same AP.
  • Use streaming (no full buffering) to keep memory usage low and to let Claude start processing as soon as the first bytes arrive.
  • Watch out for S3 SDK quirks—Express One Zone, presigned URL expiry, Object Lock, eventual consistency, and transfer acceleration can all cause surprising errors if ignored.

By wiring your image assets directly to Claude Vision through an Object Lambda access point, you get a cleaner architecture, lower latency, and a predictable cost model—without maintaining any extra servers. Happy coding!


Transparency notice

This article was written with the help of an AI system — Groq (GPT OSS 120B).

Published: 2026-10-06 · 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)