MongoDB & CachingApp RouterSEO & Social Cards

Optimizing Social Image Caching with MongoDB

A strategy to avoid regenerating the same OpenGraph image on every click, using a NoSQL database to store the image state or URL.

OG
OGCraft Team
Developer Relations
August 18, 2026
3 min read
Next.js App Router Guide
1200 × 630 px
OpenGraph Preview Card

Optimizing Social Image Caching with MongoDB

OGCraft API • Auto Generated
ogcraft.dev

Dynamic OpenGraph images are incredibly powerful for engagement, but regenerating the same image repeatedly for the same content is a massive waste of server resources. When a piece of content goes viral on Hacker News or X (Twitter), the sudden influx of HTTP requests can easily overwhelm your generation service, leading to timeouts and missing preview cards.

While Edge CDNs (like Cloudflare) provide the first line of defense, a robust backend caching layer is required for dynamically generated parameterized images. In this exhaustive guide, we'll explore how to architect a highly efficient caching layer using MongoDB. We'll detail cache verification, invalidation logic, and utilizing TTL (Time-To-Live) indexes for optimal, self-cleaning performance.


1

Designing the Data Schema

To efficiently cache images, we need a MongoDB schema that stores the unique fingerprint of the image parameters, the URL of the generated image (stored on Amazon S3, R2, or a CDN), and an expiration date.

src/models/ImageCache.ts
typescript
import mongoose, { Schema, Document } from 'mongoose';

export interface IImageCache extends Document {
  hash: string;
  imageUrl: string;
  templateId: string;
  generationTimeMs: number;
  createdAt: Date;
}

const ImageCacheSchema = new Schema({
  // A SHA-256 hash of all parameters used to generate the image
  hash: { type: String, required: true, unique: true, index: true },
  
  // The persistent storage URL (e.g., S3 bucket URL)
  imageUrl: { type: String, required: true },
  
  // Useful for analytics and targeted invalidation
  templateId: { type: String, index: true },
  
  // Performance tracking
  generationTimeMs: { type: Number },
  
  // TTL index: MongoDB will automatically delete documents older than 30 days
  createdAt: { type: Date, default: Date.now, expires: '30d' }
});

export const ImageCache = mongoose.models.ImageCache || mongoose.model<IImageCache>('ImageCache', ImageCacheSchema);

The expires: '30d' property is the secret sauce here. It automatically creates a TTL (Time-To-Live) index in MongoDB. A background thread in the MongoDB engine sweeps through the collection every 60 seconds, deleting documents where the createdAt date is older than 30 days. This keeps your database lean without writing custom cron jobs.


2

Cache Verification & Hashing Logic

Before kicking off a costly CPU-bound image generation, we compute a deterministic hash of the incoming parameters and query MongoDB. If a cached version exists, we instantly redirect the user to the S3 URL.

src/api/generate.ts
typescript
import crypto from 'crypto';
import { ImageCache } from '../models/ImageCache';
import { generateAndUploadImage } from '../services/image';

export async function handleImageRequest(req, res) {
  const { title, author, templateId, theme = 'light' } = req.query;

  // 1. Create a deterministic unique hash
  // Sorting keys ensures {a:1, b:2} produces the same hash as {b:2, a:1}
  const paramsString = JSON.stringify({ title, author, templateId, theme }, Object.keys({ title, author, templateId, theme }).sort());
  const hash = crypto.createHash('sha256').update(paramsString).digest('hex');

  // 2. Check the cache
  const cached = await ImageCache.findOne({ hash }).lean();
  
  if (cached) {
    // 302 Found: Redirects the bot to the persistent CDN URL
    return res.redirect(302, cached.imageUrl);
  }

  // 3. Generate image (expensive operation)
  const startTime = Date.now();
  const imageUrl = await generateAndUploadImage({ title, author, templateId, theme });
  const generationTimeMs = Date.now() - startTime;

  // 4. Save to cache asynchronously (do not block the response)
  ImageCache.create({ hash, imageUrl, templateId, generationTimeMs }).catch(console.error);

  // Return the newly generated URL
  return res.redirect(302, imageUrl);
}

Using a 302 Found HTTP redirect is a best practice. Social scrapers (like Twitterbot or LinkedInBot) follow redirects perfectly. This means your Node.js server never actually streams the heavy PNG payload; it simply points the scraper to your S3 bucket, saving massive amounts of egress bandwidth.


3

Handling Cache Stampedes

A "Cache Stampede" occurs when an image expires or hasn't been generated yet, and 50 concurrent requests arrive at the exact same millisecond. Without protection, your server will attempt to generate the exact same image 50 times simultaneously, crashing the CPU.

To solve this, implement an in-memory Promise Cache during generation:

src/services/promise-cache.ts
typescript
const pendingGenerations = new Map<string, Promise<string>>();

export async function getOrGenerateImage(hash: string, params: any) {
  // Check if this hash is already being generated by another request
  if (pendingGenerations.has(hash)) {
    console.log(`Stampede prevented for ${hash}, awaiting existing promise.`);
    return pendingGenerations.get(hash);
  }

  // Otherwise, start the generation and store the promise
  const generationPromise = async () => {
    try {
      const url = await generateAndUploadImage(params);
      await ImageCache.create({ hash, imageUrl: url });
      return url;
    } finally {
      // Clean up the promise map once done
      pendingGenerations.delete(hash);
    }
  }();

  pendingGenerations.set(hash, generationPromise);
  return generationPromise;
}
Impact:

Combining MongoDB persistent caching with an in-memory Promise Cache ensures that no matter how viral a link goes, your generator only renders the image exactly once. This architecture can reduce infrastructure costs by over 90% for high-traffic environments.

Conclusion

Dynamic OpenGraph images are no longer an optional luxury—they are a critical component of modern web SEO and social media marketing. Regardless of your stack, injecting custom social cards into your web pages is essential for standing out.

Ready to take your social share preview images to the next level without maintaining complex canvas code?

Start Generating Dynamic OG Images

Build Stunning OG Cards with OGCraft

Customize templates in real-time in our interactive Playground and fetch high-performance OpenGraph images via a simple API URL.