🕮6 min read · 1,104 words
Amazon S3 recently shipped a feature that sounds minor but changes how you think about file storage: Object Annotations. You can now attach rich, queryable context directly to S3 objects — and query across that context without touching the files themselves.
This is different from S3’s existing metadata in ways that matter. Here’s what changed and when it actually helps.
What S3 Metadata Could Do Before
S3 has always had two types of metadata you could attach to objects:
System metadata: Things S3 manages — Content-Type, Content-Length, Last-Modified, ETag. You read these; S3 sets them.
User-defined metadata: Custom key-value pairs you add when uploading. Limited to simple strings, limited total size, and critically — not queryable. If you wanted to find all objects with a specific metadata value, you had to list all objects and filter manually.
# Old way — add metadata on upload (strings only, not queryable)
aws s3 cp file.pdf s3://my-bucket/ \
--metadata "client_id=C001,status=processed,uploaded_by=atul"
# To find all objects where client_id=C001 — you had to do this
aws s3api list-objects --bucket my-bucket | \
jq '.Contents[].Key' | \
xargs -I{} aws s3api head-object --bucket my-bucket --key {} | \
# ... filter by metadata... horrible at scale
What Object Annotations Add
Annotations are a separate layer from metadata — structured, typed, and queryable:
- Structured types: Not just strings — numbers, booleans, timestamps, arrays, nested objects
- Larger payload: Up to 2KB per annotation set vs 2KB total for all user metadata
- Queryable: S3 Select and S3 Inventory can filter and query based on annotations
- Separate access control: You can grant access to read annotations without granting access to the object itself
- Updatable independently: Update annotations without copying or replacing the object
# Attach annotations to an existing object
aws s3api put-object-annotations \
--bucket my-bucket \
--key documents/invoice-2026-001.pdf \
--annotations '{
"client_id": "C001",
"client_name": "Agrawal Steel Industries",
"document_type": "invoice",
"amount": 245000,
"currency": "INR",
"status": "pending_approval",
"due_date": "2026-08-15",
"approver_id": "user_42",
"tags": ["gst", "b2b", "steel"],
"processed": false
}'
# Query objects by annotation — no Lambda, no DynamoDB index needed
aws s3api list-objects-with-annotations \
--bucket my-bucket \
--annotation-filter '{
"status": {"eq": "pending_approval"},
"amount": {"gt": 100000},
"due_date": {"lte": "2026-08-01"}
}'
What This Replaces
Before Annotations, the common pattern for “find S3 files by business context” was:
- Store file in S3
- Store file metadata in DynamoDB or RDS with the S3 key
- Query the database to find relevant keys
- Fetch from S3 using those keys
This works but adds architectural complexity — you now have two systems that need to stay in sync. If you update the file (re-upload), you need to update the database record. If the database is inconsistent, you can end up with orphaned records or stale state.
Annotations keep the context with the file. The source of truth is the object itself.
Practical Use Cases
Document Management Systems
// Laravel — attach annotations when storing a document
use Aws\S3\S3Client;
class DocumentService
{
public function __construct(private S3Client $s3) {}
public function store(UploadedFile $file, array $context): string
{
$key = 'documents/' . Str::uuid() . '.' . $file->getClientOriginalExtension();
// Upload the file
$this->s3->putObject([
'Bucket' => config('filesystems.disks.s3.bucket'),
'Key' => $key,
'Body' => $file->getContent(),
'ContentType' => $file->getMimeType(),
]);
// Attach rich annotations separately
$this->s3->putObjectAnnotations([
'Bucket' => config('filesystems.disks.s3.bucket'),
'Key' => $key,
'Annotations' => [
'client_id' => $context['client_id'],
'document_type' => $context['type'],
'amount' => $context['amount'] ?? null,
'status' => 'uploaded',
'uploaded_by' => auth()->id(),
'uploaded_at' => now()->toIso8601String(),
'fiscal_year' => date('Y') . '-' . (date('Y') + 1),
'tags' => $context['tags'] ?? [],
],
]);
return $key;
}
public function findPendingApprovals(string $clientId): array
{
$result = $this->s3->listObjectsWithAnnotations([
'Bucket' => config('filesystems.disks.s3.bucket'),
'Prefix' => 'documents/',
'AnnotationFilter' => [
'client_id' => ['eq' => $clientId],
'status' => ['eq' => 'pending_approval'],
],
]);
return $result['Objects'] ?? [];
}
}
Media Asset Management
# Marketing team uploads images — annotate with campaign context
aws s3api put-object-annotations \
--bucket assets-bucket \
--key images/campaign-diwali-hero.jpg \
--annotations '{
"campaign": "diwali-2026",
"brand": "softcrony",
"format": "hero",
"dimensions": {"width": 1920, "height": 1080},
"color_profile": "sRGB",
"approved": true,
"approved_by": "design_lead",
"expires_at": "2026-11-15",
"usage_rights": ["web", "social", "email"]
}'
# Find all approved hero images for Diwali campaign
aws s3api list-objects-with-annotations \
--bucket assets-bucket \
--annotation-filter '{
"campaign": {"eq": "diwali-2026"},
"format": {"eq": "hero"},
"approved": {"eq": true}
}'
Compliance and Audit Trails
# Attach processing audit trail to sensitive documents
aws s3api put-object-annotations \
--bucket compliance-bucket \
--key reports/financial-Q2-2026.xlsx \
--annotations '{
"classification": "confidential",
"data_category": "financial",
"retention_years": 7,
"retention_until": "2033-07-25",
"processing_log": [
{"action": "created", "user": "cfo@company.com", "timestamp": "2026-07-25T09:00:00Z"},
{"action": "reviewed", "user": "auditor@firm.com", "timestamp": "2026-07-25T14:30:00Z"}
],
"dpdp_category": "financial_data",
"deletion_request_pending": false
}'
S3 Annotations vs DynamoDB — When to Use Each
| Scenario | Use Annotations | Use DynamoDB |
|---|---|---|
| Context is specific to the file | ✅ | ❌ |
| Context is shared across files | ❌ | ✅ |
| Simple filtering queries | ✅ | ✅ |
| Complex joins across entities | ❌ | ✅ |
| Context must stay with object (compliance) | ✅ | ❌ |
| High-frequency updates to context | ⚠️ Check pricing | ✅ |
| Reducing architectural complexity | ✅ | ❌ |
Pricing Considerations
S3 Annotations have their own request pricing — separate from standard S3 GET/PUT requests. Key points:
- PutObjectAnnotations: priced per request (check current AWS pricing)
- GetObjectAnnotations: separate from GetObject — you can read annotations without paying for object retrieval
- ListObjectsWithAnnotations: billed differently from ListObjects
- Annotation storage is separate from object storage cost
For most use cases — storing annotations on documents and querying them occasionally — the cost is negligible. For high-frequency annotation updates on millions of objects, model the costs carefully before adoption.
Setting This Up in Laravel with Flysystem
// config/filesystems.php — custom S3 client with annotations support
'disks' => [
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'ap-south-1'), // Mumbai
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
],
],
// app/Services/S3AnnotationService.php
use Aws\S3\S3Client;
class S3AnnotationService
{
public function __construct(
private readonly S3Client $client
) {}
public function annotate(string $key, array $annotations): void
{
$this->client->putObjectAnnotations([
'Bucket' => config('filesystems.disks.s3.bucket'),
'Key' => $key,
'Annotations' => $annotations,
]);
}
public function getAnnotations(string $key): array
{
$result = $this->client->getObjectAnnotations([
'Bucket' => config('filesystems.disks.s3.bucket'),
'Key' => $key,
]);
return $result['Annotations'] ?? [];
}
public function updateStatus(string $key, string $status): void
{
// Update single annotation without fetching entire object
$existing = $this->getAnnotations($key);
$this->annotate($key, array_merge($existing, [
'status' => $status,
'updated_at' => now()->toIso8601String(),
]));
}
}
S3 Object Annotations is one of those AWS features that doesn’t look revolutionary in the announcement but quietly removes an entire class of architectural decisions from your applications. Worth knowing about before you design your next file storage system.
If you’re building document management, asset pipelines, or compliance systems on AWS and need architecture advice, our team at Softcrony can help you design the right approach.
Leave a comment