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
uploadWithNodeHttpto 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 decryptionurlordirectPath– the upload locationfileSha256– 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:
- Baileys computes a
cacheableKeyfrom the media's SHA-256 hash and mime type (mediaMessageSHA256B64) - Stores the raw, serialized protobuf (
WAProto.Message.encode) in your providedmediaCache - 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):
- Extract
mediaKey,directPath, andurlfrom the message protobuf - Call
downloadContentFromMessagewith the media type specifier - Baileys derives decryption keys via
getMediaKeys, builds the download URL viagetUrlFromDirectPath, and streams throughdownloadEncryptedContentwith 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
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
MaporLRUCachewith Redis, S3, or a database-backedCacheStore - 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), andsendMessage(transmit)—all orchestrated automatically when you pass a buffer tosock.sendMessage() - Upload-side caching via
mediaCacheprevents 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
downloadContentFromMessagefrommessages-media.ts:L31-L46with 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →