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 requestPlaceholderResend in src/Socket/messages-recv.ts, caching metadata and awaiting fresh credentials.

  • Status broadcasts older than 24 hours: Automatically skipped using STATUS_EXPIRY_SECONDS check; no recovery attempted.

  • Outgoing failed uploads: The entire upload pipeline restarts with fresh key derivation in getMediaKeys and new HTTP upload via uploadWithNodeHttp.

  • 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:

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 →