Draft image alt text with AWS Rekognition and Node.js
Use AWS Rekognition’s DetectLabels operation to suggest objects and scenes that an editor can
consider when writing alt text. This walkthrough prints image-specific label suggestions with a
reviewRequired flag, then adds local-file caching and a small sequential batch runner.
Why automate alt text?
Labels can help you start a description, but they cannot determine an image’s purpose on a page.
Follow the W3C alt decision tree to decide
whether to describe the image, explain its link or button function, or use an empty alternative for
a decorative image. Keep suggestions out of the alt attribute until someone has reviewed the
image in context.
AWS Rekognition and Node.js integration
DetectLabels
returns labels and confidence scores, rather than a contextual caption. Labels are returned in
English and can include several names for one object, such as a specific object and its parent
category. This example keeps up to five names at a chosen confidence threshold. For reading words
inside an image, use Rekognition’s separate DetectText operation.
Setting up your environment
You need Bash, Node.js 24 LTS, npm, and an authenticated AWS profile
or IAM role allowed to call rekognition:DetectLabels. The examples were tested with Node.js
24.21.0 and 26.8.1, and AWS SDK v3’s @aws-sdk/client-rekognition 3.1148.0. These are tested
versions, not an intrinsic minimum for the API; use a maintained Node.js release. Sending an image
to AWS can incur a charge.
Use your organization’s existing authentication method and the SDK’s
default credential provider chain.
For a local profile, sign in first and set AWS_PROFILE in the commands below. For an IAM role,
omit that variable. Temporary environment credentials need AWS_SESSION_TOKEN as well as the
access key and secret; keep their values out of source files and terminal output.
Run this from the directory where you want a new image-labels project. The subshell preserves your
current directory, and mkdir refuses to overwrite an existing project. The explicit npm target
also keeps installation in the new project when its parent uses another package manager:
(
command -v node >/dev/null || { printf 'Install Node.js first.\n' >&2; exit 1; }
command -v npm >/dev/null || { printf 'Install npm first.\n' >&2; exit 1; }
[ -z "${NODE_OPTIONS:-}" ] || { printf 'Use a shell without NODE_OPTIONS.\n' >&2; exit 1; }
mkdir image-labels && cd image-labels &&
printf '{"private":true,"type":"module"}\n' > package.json &&
npm install --prefix "$PWD" --workspaces=false --ignore-scripts --save-exact \
@aws-sdk/client-rekognition@3.1148.0
)
Save the files below in image-labels. If installation fails after creating the directory, correct
the reported problem and rerun the npm install command from inside that directory. Keep the
existing files; rerunning the entire creation block intentionally refuses that directory.
Image requirements
Use JPEG or PNG images. AWS documents a maximum of 5 MB for direct bytes and 15 MB for an S3 object;
DetectLabels requires each dimension to be between 80 and 10,000 pixels. See the
Rekognition quotas for the current
limits. The code sends your original bytes without resizing, decoding, or changing color profiles.
A successful request does not verify that the image is undamaged or that the labels are accurate.
Implementing label suggestions
Save this as image-labels/labels.mjs. The SDK accepts the file buffer directly; do not manually
base64-encode it. Leaving credentials unset preserves profile and role authentication. Setting
maxAttempts: 1 disables automatic retries so a failed analysis remains a failed invocation.
import { DetectLabelsCommand, RekognitionClient } from '@aws-sdk/client-rekognition'
import fs from 'node:fs'
const client = new RekognitionClient({ region: process.env.AWS_REGION, maxAttempts: 1 })
async function detectImageLabels(image, minConfidence = 75) {
if (!Number.isFinite(minConfidence) || minConfidence < 0 || minConfidence > 100) {
throw new Error('Confidence must be a number between 0 and 100.')
}
const response = await client.send(new DetectLabelsCommand({
Image: image,
Features: ['GENERAL_LABELS'],
MaxLabels: 5,
MinConfidence: minConfidence,
}))
return (response.Labels ?? [])
.map((label) => label.Name)
.filter((name) => typeof name === 'string' && name.trim().length > 0)
}
async function suggestImageLabels(imagePath, minConfidence = 75) {
const imageBytes = fs.readFileSync(imagePath)
return detectImageLabels({ Bytes: imageBytes }, minConfidence)
}
function reviewRecord(source, labelSuggestions, minConfidence = 75) {
return { source, minConfidence, reviewRequired: true, labelSuggestions }
}
export { client, detectImageLabels, reviewRecord, suggestImageLabels }
Save the entry point as image-labels/review.mjs:
import { client, reviewRecord, suggestImageLabels } from './labels.mjs'
try {
const [imagePath, confidence = '75', ...extra] = process.argv.slice(2)
if (!imagePath || extra.length > 0 || !process.env.AWS_REGION) {
throw new Error('Supply AWS_REGION and an image path, with optional confidence.')
}
const minConfidence = Number(confidence)
const labels = await suggestImageLabels(imagePath, minConfidence)
console.log(JSON.stringify(reviewRecord(imagePath, labels, minConfidence)))
} catch {
console.error('Unable to retrieve image label suggestions. Check the path, region, credentials, and image.')
process.exitCode = 1
} finally {
client.destroy()
}
Choose your region and replace MY_PROFILE and ./example.jpg. Paths are relative to the directory
where you run Node.js; quote filenames containing spaces:
AWS_REGION=us-east-1 AWS_PROFILE=MY_PROFILE node image-labels/review.mjs './example.jpg'
The JSON contains the selected source, the threshold, reviewRequired: true, and
labelSuggestions. Inspect those names against your actual image. An empty list is a successful
analysis with no usable labels, so it still needs review. File, credential, and provider failures
print a diagnostic to stderr, produce no success record, and exit nonzero. No field contains finished
alt text.
Processing images from S3
For an existing S3 image, the bucket’s region must match the Rekognition client’s region, and the
calling identity needs s3:GetObject for the object. See AWS’s
S3 image guide and
IAM guidance.
This example reads the current object at the key and does not select a historical version.
Append this to image-labels/labels.mjs:
async function suggestImageLabelsFromS3(bucket, key, minConfidence = 75) {
return detectImageLabels({ S3Object: { Bucket: bucket, Name: key } }, minConfidence)
}
export { suggestImageLabelsFromS3 }
Save image-labels/review-s3.mjs:
import { client, reviewRecord, suggestImageLabelsFromS3 } from './labels.mjs'
try {
const [bucket, key, confidence = '75', ...extra] = process.argv.slice(2)
if (!bucket || !key || extra.length > 0 || !process.env.AWS_REGION) {
throw new Error('Supply AWS_REGION, a bucket, and an object key, with optional confidence.')
}
const minConfidence = Number(confidence)
const labels = await suggestImageLabelsFromS3(bucket, key, minConfidence)
console.log(JSON.stringify(reviewRecord({ bucket, key }, labels, minConfidence)))
} catch {
console.error('Unable to retrieve S3 image label suggestions. Check the key, region, credentials, and permissions.')
process.exitCode = 1
} finally {
client.destroy()
}
Replace the bucket and key with your own existing image. Pass the key literally, including spaces or percent characters; do not URL-encode it. This S3 path is uncached, so it analyzes the current object on each invocation:
AWS_REGION=us-east-1 AWS_PROFILE=MY_PROFILE node image-labels/review-s3.mjs my-bucket 'images/example.jpg'
Handling edge cases and errors
A failed request is different from an empty label list. The helpers reject failures; the entry points report them without exposing raw AWS responses or credentials. For troubleshooting, check the local path first, then authentication and region. For S3, also check the exact key and object-read permissions. AWS’s DetectLabels errors cover unsupported formats, invalid parameters, inaccessible S3 objects, and service or quota failures. Correct the cause before rerunning the command. Each retry is another possible analysis.
Enhancing functionality
Implement caching
For local images, cache successful names by their contents and request settings. This optional
section assumes an already running Redis server and its connection URL. It was tested with
Redis 7.4.11 and the current redis client, 6.3.0. Install the client in the existing project:
test -f image-labels/package.json &&
npm install --prefix "$PWD/image-labels" --workspaces=false --ignore-scripts --save-exact redis@6.3.0
Save image-labels/cache.mjs. The identity includes the region, content hash, confidence threshold,
and fixed label settings. Read and analyze the same buffer so a changed file cannot reuse labels
from an earlier version. Store only after a successful provider response; malformed or mismatched
cache records fail visibly rather than being treated as labels:
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import { createClient } from 'redis'
import { detectImageLabels } from './labels.mjs'
const redisClient = createClient({
url: process.env.REDIS_URL,
socket: { connectTimeout: 5000, reconnectStrategy: false },
})
redisClient.on('error', () => console.error('Redis connection error.'))
async function getCachedOrSuggestImageLabels(imagePath, minConfidence = 75) {
if (!Number.isFinite(minConfidence) || minConfidence < 0 || minConfidence > 100) {
throw new Error('Confidence must be a number between 0 and 100.')
}
const imageBytes = fs.readFileSync(imagePath)
const imageHash = createHash('sha256').update(imageBytes).digest('hex')
const identity = JSON.stringify({
region: process.env.AWS_REGION, imageHash, minConfidence,
features: ['GENERAL_LABELS'], maxLabels: 5,
})
const cacheKey = `image_labels:v2:${createHash('sha256').update(identity).digest('hex')}`
const cached = await redisClient.get(cacheKey)
if (cached !== null) {
const record = JSON.parse(cached)
if (record.identity !== identity || !Array.isArray(record.labels) ||
!record.labels.every((name) => typeof name === 'string' && name.trim().length > 0)) {
throw new Error('Invalid image-label cache record.')
}
return record.labels
}
const labels = await detectImageLabels({ Bytes: imageBytes }, minConfidence)
await redisClient.set(cacheKey, JSON.stringify({ identity, labels }), { EX: 60 * 60 * 24 })
return labels
}
export { getCachedOrSuggestImageLabels, redisClient }
Save image-labels/review-cached.mjs:
import { client, reviewRecord } from './labels.mjs'
let redisClient
try {
const [imagePath, confidence = '75', ...extra] = process.argv.slice(2)
if (!imagePath || extra.length > 0 || !process.env.AWS_REGION || !process.env.REDIS_URL) {
throw new Error('Supply AWS_REGION, REDIS_URL, and an image path, with optional confidence.')
}
const cache = await import('./cache.mjs')
redisClient = cache.redisClient
await redisClient.connect()
const minConfidence = Number(confidence)
const labels = await cache.getCachedOrSuggestImageLabels(imagePath, minConfidence)
console.log(JSON.stringify(reviewRecord(imagePath, labels, minConfidence)))
} catch {
console.error('Unable to retrieve cached image label suggestions. Check Redis and the image prerequisites.')
process.exitCode = 1
} finally {
if (redisClient?.isOpen) await redisClient.close()
client.destroy()
}
Supply your Redis URL and run the same command twice. The second run should read the stored result without another Rekognition analysis. The 24-hour TTL limits reuse, but does not pin AWS’s model version. Increment the cache namespace when changing the projection or fixed settings. This example fails if Redis is unavailable; it does not silently switch to an uncached billed request.
AWS_REGION=us-east-1 AWS_PROFILE=MY_PROFILE REDIS_URL=redis://127.0.0.1:6379 node image-labels/review-cached.mjs './example.jpg'
Batch processing
For a small list, process one file at a time and retain each file’s result. A failed member does not
prevent later files from being analyzed, but makes the overall command exit nonzero. This runner is
uncached and sends one analysis per readable image. Save image-labels/review-batch.mjs:
import { client, reviewRecord, suggestImageLabels } from './labels.mjs'
async function batchProcessImages(imagePaths) {
const results = []
for (const imagePath of imagePaths) {
try {
const labels = await suggestImageLabels(imagePath)
results.push(reviewRecord(imagePath, labels))
} catch {
results.push({ source: imagePath, reviewRequired: true, status: 'failed' })
}
}
return results
}
try {
const imagePaths = process.argv.slice(2)
if (imagePaths.length === 0 || !process.env.AWS_REGION) {
throw new Error('Supply AWS_REGION and at least one image path.')
}
const results = await batchProcessImages(imagePaths)
console.log(JSON.stringify(results))
if (results.some((result) => result.status === 'failed')) process.exitCode = 1
} catch {
console.error('Unable to run the image-label batch. Check the region and arguments.')
process.exitCode = 1
} finally {
client.destroy()
}
Inspect each record’s source before using its suggestions. The failed record has no label data:
AWS_REGION=us-east-1 AWS_PROFILE=MY_PROFILE node image-labels/review-batch.mjs './image1.jpg' './image2.jpg' './image3.jpg'
Confidence threshold tuning
Pass a threshold between 0 and 100 as the last argument to a single-image command. For example, request a minimum confidence of 90%:
AWS_REGION=us-east-1 AWS_PROFILE=MY_PROFILE node image-labels/review.mjs './example.jpg' 90
A higher threshold can remove useful labels as well as uncertain ones; even a high-confidence prediction can be wrong. The cache helper includes this value in its identity, so a request at 90% cannot reuse a result requested at 75%.
Closing thoughts
Use the suggestions as prompts for an editor who can see the image and its surrounding content. Confidence describes the model’s prediction, not accessibility quality. If your application also needs other image-analysis operations, explore our 🤖 Artificial Intelligence Service.
