What Happens When Media Messages Expire in Baileys and How Re‑upload Is Handled
Baileys handles expired media messages through placeholder-resend requests for incoming media and automatic re‑upload with fresh credentials for outgoing media, all governed by WhatsApp Web's 24‑hour lifetime rules.
The Baileys library implements the same media lifecycle semantics as the official WhatsApp Web client. When a media URL expires or becomes unreachable, the library executes different recovery strategies depending on whether the message is incoming or outgoing. This article examines the expiration checks, placeholder-resend mechanism, and re‑upload retry logic as implemented in the WhiskeySockets/Baileys codebase.
Incoming Media Expiration: Placeholder Resend
When Baileys receives a media message with a stale or missing download URL, it initiates a placeholder-resend request to recover the content from the connected phone.
When URLs Expire During Reception
Baileys detects expired media in src/Socket/messages-recv.ts (lines 1650‑1689). The logic triggers when msg.message.imageMessage?.url (or equivalent for video, audio, document, sticker) is falsy:
// src/Socket/messages-recv.ts – placeholder resend logic
if (msg.message && !msg.message?.imageMessage?.url) {
const cleanKey: proto.IMessageKey = {
remoteJid: msg.key.remoteJid,
fromMe: msg.key.fromMe,
id: msg.key.id,
participant: msg.key.participant,
};
const msgData: Partial<WAMessage> = {
key: msg.key,
messageTimestamp: msg.messageTimestamp,
pushName: msg.pushName,
participant: msg.participant,
verifiedBizName: msg.verifiedBizName,
};
requestPlaceholderResend(cleanKey, msgData)
.then(requestId => {
if (requestId && requestId !== 'RESOLVED') {
ev.emit('messages.update', [{
key: msg.key,
update: { messageStubParameters: [NO_MESSAGE_FOUND_ERROR_TEXT, requestId] },
}]);
}
})
.catch(err => logger.warn({ err, msgId: msg.key.id }, 'failed to request placeholder resend'));
}
The requestPlaceholderResend function sends a PDO (placeholder data object) to the device. The phone responds with a fresh media node containing updated url, mediaKey, and directPath values.
Status Broadcasts: Hard 24‑Hour Expiry
Status (story) broadcasts follow stricter rules. Baileys computes message age using unixTimestampSeconds() - messageTimestamp and compares against STATUS_EXPIRY_SECONDS (defined in src/Defaults/index.ts as 86,400 seconds / 24 hours). Messages exceeding this threshold are acknowledged and discarded without retry:
// src/Socket/messages-recv.ts – lines 1697‑1707
if (isStatusBroadcast && messageAge > STATUS_EXPIRY_SECONDS) {
// Skip expired status messages entirely
await sendMessageAck(msg);
return;
}
Outgoing Media: Re‑upload on Failure
When sending media, Baileys generates temporary upload credentials that may expire before transmission completes. The library implements automatic re‑upload with fresh key derivation.
Upload Flow and Retry Mechanism
The sendMediaMessage pattern in src/Socket/messages-send.ts coordinates with utilities in src/Utils/messages-media.ts:
// Simplified re‑upload pattern for outgoing media
async function sendMediaMessage(
media: WAMediaUpload,
type: MediaType,
logger?: ILogger,
) {
// 1️⃣ Extract raw file data and compute SHA-256 hash
const { filePath, fileSha256, fileLength } = await getRawMediaUploadData(
media,
type,
logger
);
// 2️⃣ Upload to WhatsApp media server via HTTP
const uploadResp = await uploadWithNodeHttp({
filePath,
fileSha256,
fileLength,
mediaType: type,
logger,
});
// 3️⃣ Construct message with fresh credentials
const mediaMessage = {
url: uploadResp.url,
mediaKey: uploadResp.mediaKey,
directPath: uploadResp.directPath,
// …additional fields
};
// 4️⃣ Retry on server-side rejection
try {
await sock.sendMessage(targetJid, { [type + 'Message']: mediaMessage });
} catch (e) {
logger?.warn({ e }, 'Media upload failed, retrying...');
return sendMediaMessage(media, type, logger); // Fresh upload attempt
}
}
Key Functions in the Re‑upload Pipeline
| Function | Location | Purpose |
|---|---|---|
getRawMediaUploadData |
src/Utils/messages-media.ts |
Prepares file stream, computes fileSha256, returns temporary path |
getMediaKeys |
src/Utils/messages-media.ts |
Derives HKDF-based encryption keys from mediaKey |
uploadWithNodeHttp |
src/Utils/messages-media.ts |
Executes HTTPS POST to WhatsApp's media server |
uploadWithFetch |
src/Utils/messages-media.ts |
Alternative fetch-based uploader |
If the server rejects an upload (HTTP 404, auth failure, or expired URL), the catch block re-invokes the entire pipeline—generating new keys, new hashes, and a new upload URL.
Media Key Handling and Decryption Failures
Media keys are time-sensitive credentials tied to specific upload sessions. Baileys derives decryption keys using HKDF in getMediaKeys:
// src/Utils/messages-media.ts – key derivation
function getMediaKeys(mediaKey: Buffer, mediaType: MediaType) {
const expanded = hkdf(mediaKey, 112, { info: `WhatsApp Media Keys ${mediaType}` });
return {
iv: expanded.slice(0, 16),
cipherKey: expanded.slice(16, 48),
macKey: expanded.slice(48, 80),
};
}
When decryption fails due to missing or malformed keys, Baileys falls back to the placeholder-resend flow described earlier, treating the message as expired and requesting fresh credentials from the phone.
Logging and Observability
All expiration and retry events flow through the structured logger:
- Debug: Successful placeholder request initiation
- Warn: Failed placeholder resend, upload retry attempts
- Error: Unexpected decryption failures or unrecoverable errors
The messages.update event emitted during placeholder resend allows applications to track pending media recovery via the requestId in messageStubParameters.
Summary
-
Incoming expired media: Baileys sends a placeholder-resend request to the phone via
requestPlaceholderResendinsrc/Socket/messages-recv.ts, caching metadata and awaiting fresh credentials. -
Status broadcasts older than 24 hours: Automatically skipped using
STATUS_EXPIRY_SECONDScheck; no recovery attempted. -
Outgoing failed uploads: The entire upload pipeline restarts with fresh key derivation in
getMediaKeysand new HTTP upload viauploadWithNodeHttp. -
Decryption failures: Trigger the same placeholder-resend mechanism as expired URLs.
Frequently Asked Questions
How long does WhatsApp allow media messages to remain accessible?
WhatsApp enforces a 24‑hour lifetime for status broadcast media, defined as STATUS_EXPIRY_SECONDS in src/Defaults/index.ts. Standard chat media follows similar server-side expiration rules, though Baileys relies on the phone to provide fresh URLs via placeholder resend rather than enforcing a hard client-side cutoff.
Can I disable automatic re‑upload for outgoing media?
No. The retry logic is embedded in the send path and triggers on any upload failure. To implement custom behavior, wrap sendMessage calls and intercept specific error codes before the built-in retry executes.
What causes a placeholder-resend request to fail?
Failure occurs when the phone is offline, the original message was deleted on the device, or the message exceeds WhatsApp's server-side retention window. The promise rejects and Baileys logs a warning with the message ID; no further automatic recovery is attempted.
Does Baileys cache media keys between sessions?
No. Media keys are derived per-message using HKDF and are not persisted. Each placeholder-resend or re‑upload generates fresh keys, ensuring cryptographic isolation between upload attempts.
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 →