How to Handle Sending and Receiving Media Messages with Baileys (Including Caching)

Baileys processes every media message through a three-step pipeline—encrypt and prepare with encryptedStream, upload via uploadMedia, then send with sendMessage—while the optional mediaCache store prevents duplicate uploads and the downloadContentFromMessage helper enables local caching of received files.

The Baileys library by WhiskeySockets is a popular WhatsApp Web API for Node.js that handles media differently than text. Understanding how to send and receive media messages with Baileys—including its built-in caching mechanisms—is essential for building performant bots and integrations. This guide walks through the complete media pipeline using actual source code from the repository.

The Media Message Pipeline in Baileys

Baileys implements a unified pipeline for all media types—images, videos, audio, documents, and stickers. The flow is consistent regardless of format, with specialized handling in src/Utils/messages-media.ts.

Step 1: Encrypt and Prepare the Media

When you call sendMessage with media, Baileys first encrypts the raw bytes. In [src/Utils/messages-media.ts](https://github.com/WhiskeySockets/Baileys/blob/master/src/Utils/messages-media.ts#L85-L99), the encryptedStream function:

  • Generates a cryptographically random mediaKey
  • Expands it using HKDF via getMediaKeys
  • Streams the file through AES-256-CBC encryption
  • Writes the encrypted output to a temporary file
// Encrypted file is written to encFilePath for upload
const { encFilePath, ... } = await encryptedStream({
  media: buffer,
  mediaType: 'image' // or 'video', 'audio', 'document', etc.
})

Step 2: Upload the Encrypted File

The encrypted file is uploaded through messages-media.ts:L165-L176 via uploadMedia. Baileys selects the safest upload implementation to avoid known issues:

  • Node.js environments: Uses uploadWithNodeHttp to bypass the Undici memory-bloat bug
  • Other environments: Falls back to standard Fetch

This selection logic appears at messages-media.ts:L162-L166. The upload returns either a directPath (preferred) or a public url for the encrypted blob.

Step 3: Send the Final Message

The sendMessage implementation in [src/Socket/messages-send.ts](https://github.com/WhiskeySockets/Baileys/blob/master/src/Socket/messages-send.ts) constructs a WAGenericMediaMessage protobuf containing:

  • mediaKey – the encryption key needed for decryption
  • url or directPath – the upload location
  • fileSha256 – hash for integrity verification
  • Metadata: caption, thumbnail, mimetype, file length

This protobuf is serialized and transmitted over the WhatsApp Web WebSocket.

Upload-Side Caching: Avoid Duplicate Uploads

Baileys provides an optional upload-side cache to prevent re-uploading identical files. This is particularly valuable when broadcasting the same image to multiple chats or retrying failed sends.

How Upload Caching Works

In src/Utils/messages.ts:L151-L218, after a successful upload:

  1. Baileys computes a cacheableKey from the media's SHA-256 hash and mime type (mediaMessageSHA256B64)
  2. Stores the raw, serialized protobuf (WAProto.Message.encode) in your provided mediaCache
  3. On subsequent sends with identical content, retrieves the cached protobuf and skips encryption/upload entirely

Configuring the Upload Cache

Provide any CacheStore implementation when creating your socket. The interface is defined in [src/Types/Socket.ts](https://github.com/WhiskeySockets/Baileys/blob/master/src/Types/Socket.ts):

import makeWASocket, { CacheStore } from '@whiskeysockets/baileys'

// Simple in-memory Map implementation
const mediaCache: CacheStore = new Map<string, Buffer>()

const sock = makeWASocket({
  auth: state,
  mediaCache // Enable upload caching
})

Complete Sending Example with Caching

import makeWASocket, { useSingleFileAuthState } from '@whiskeysockets/baileys'
import { readFile } from 'fs/promises'

const { state } = useSingleFileAuthState('./auth_info')
const sock = makeWASocket({
  auth: state,
  mediaCache: new Map<string, Buffer>()
})

const imgBuffer = await readFile('./promotional-banner.jpg')

// First call: encrypts, uploads, and caches the protobuf
await sock.sendMessage('123456789@s.whatsapp.net', {
  image: imgBuffer,
  caption: 'Check out our sale!'
})

// Second call with identical buffer: instant send from cache
await sock.sendMessage('987654321@s.whatsapp.net', {
  image: imgBuffer,
  caption: 'Check out our sale!'
})

The cache key is automatically derived from mediaMessageSHA256B64—no manual key management required.

Download-Side Caching: Optimize Received Media

For receiving media messages with Baileys, repeated downloads of the same content waste bandwidth and time. The downloadContentFromMessage helper in src/Utils/messages-media.ts:L31-L46 returns a stream, allowing you to implement your own caching layer.

The Download and Decryption Flow

When processing an incoming message via [messages-recv.ts](https://github.com/WhiskeySockets/Baileys/blob/master/src/Socket/messages-recv.ts):

  1. Extract mediaKey, directPath, and url from the message protobuf
  2. Call downloadContentFromMessage with the media type specifier
  3. Baileys derives decryption keys via getMediaKeys, builds the download URL via getUrlFromDirectPath, and streams through downloadEncryptedContent with on-the-fly AES-256-CBC decryption

The decryption transform handles chunked range requests and IV preparation automatically (messages-media.ts:L31-L55).

Implementing a Download Cache

Since Baileys returns a stream rather than a buffer, you consume and cache the result:

import { downloadContentFromMessage, extensionForMediaMessage } from '@whiskeysockets/baileys'
import LRUCache from 'lru-cache'

// LRU cache: max 100 items, 5 minute TTL
const downloadCache = new LRUCache<string, Buffer>({
  max: 100,
  ttl: 1000 * 60 * 5
})

sock.ev.on('messages.upsert', async ({ messages }) => {
  for (const msg of messages) {
    const media = msg.message?.imageMessage || 
                  msg.message?.videoMessage ||
                  msg.message?.documentMessage
    if (!media) continue

    const ext = extensionForMediaMessage(msg.message)
    const cacheKey = `${msg.key.remoteJid}:${msg.key.id}${ext}`

    let buffer = downloadCache.get(cacheKey)
    if (!buffer) {
      // Download and decrypt from WhatsApp CDN
      const stream = await downloadContentFromMessage({
        mediaKey: media.mediaKey!,
        directPath: media.directPath,
        url: media.url
      }, 'image') // or 'video', 'audio', 'document'

      buffer = Buffer.from(await new Response(stream as any).arrayBuffer())
      downloadCache.set(cacheKey, buffer)
      console.log('📥 Downloaded and cached:', cacheKey)
    } else {
      console.log('⚡ Cache hit:', cacheKey)
    }

    // Use buffer (save to disk, process, forward, etc.)
    await writeFile(`./downloads/${cacheKey}`, buffer)
  }
})

Key Files and Their Roles

File Responsibility
[src/Utils/messages-media.ts](https://github.com/WhiskeySockets/Baileys/blob/master/src/Utils/messages-media.ts) Core encryption (encryptedStream), upload (uploadMedia), download (downloadContentFromMessage), and thumbnail generation
[src/Utils/messages.ts](https://github.com/WhiskeySockets/Baileys/blob/master/src/Utils/messages.ts) Orchestrates the full send flow and manages mediaCache integration
[src/Socket/messages-send.ts](https://github.com/WhiskeySockets/Baileys/blob/master/src/Socket/messages-send.ts) Constructs final WAGenericMediaMessage protobuf and transmits
[src/Socket/messages-recv.ts](https://github.com/WhiskeySockets/Baileys/blob/master/src/Socket/messages-recv.ts) Parses incoming stanzas and extracts media metadata
[src/Types/Socket.ts](https://github.com/WhiskeySockets/Baileys/blob/master/src/Types/Socket.ts) Defines mediaCache?: CacheStore for socket configuration
[src/Types/Message.ts](https://github.com/WhiskeySockets/Baileys/blob/master/src/Types/Message.ts) Per-message options including cache-related flags

Performance Considerations

  • Upload cache keys are SHA-256 based—identical bytes produce identical keys across mime types
  • Download caching is consumer-implemented because Baileys cannot know your storage constraints or retention policies
  • Memory vs. external cache: For production, replace Map or LRUCache with Redis, S3, or a database-backed CacheStore
  • Range request handling: Downloads use HTTP range requests for resumability; large files stream efficiently without full memory buffering

Summary

  • Sending media requires three steps: encryptedStream (encrypt), uploadMedia (upload), and sendMessage (transmit)—all orchestrated automatically when you pass a buffer to sock.sendMessage()
  • Upload-side caching via mediaCache prevents duplicate encryption and upload; the cache stores raw protobufs keyed by SHA-256 in [messages.ts](https://github.com/WhiskeySockets/Baileys/blob/master/src/Utils/messages.ts#L151-L218)
  • Receiving media uses downloadContentFromMessage from messages-media.ts:L31-L46 with on-the-fly AES-256-CBC decryption
  • Download-side caching is your responsibility—consume the stream, store the buffer, and key by message ID for reuse
  • All media types share the same pipeline; only the protobuf wrapper and metadata differ

Frequently Asked Questions

How does Baileys generate the media encryption key?

Baileys generates a random 32-byte mediaKey in encryptedStream, then expands it using HKDF-SHA256 via getMediaKeys to produce separate keys for AES-256-CBC encryption and HMAC-SHA256 authentication.

What cache implementation should I use for production?

Any object matching the CacheStore interface works. For single-instance deployments, lru-cache with TTL is sufficient. For horizontal scaling, implement the interface with Redis, PostgreSQL, or S3-backed storage—persist Buffer values as base64 or binary.

Can I disable caching for specific messages?

Yes. The mediaCache is socket-level and optional. To bypass cache for a single send, you can temporarily mutate the socket options or implement a CacheStore that ignores set calls based on custom logic. The per-message options in [src/Types/Message.ts](https://github.com/WhiskeySockets/Baileys/blob/master/src/Types/Message.ts) do not currently include cache bypass flags.

Why does downloading return a stream instead of a buffer?

Streaming prevents memory exhaustion with large files (videos, long audio). WhatsApp media can exceed 100MB. By returning a Transform stream from downloadEncryptedContent, Baileys allows you to pipe directly to disk, process chunks, or accumulate into a cache only when needed.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →