April 16, 2026 โ€ข Version: 2026.2.6-3

Adding maxMessageAge Filter to Drop Stale Re-delivered BlueBubbles Webhooks

How to configure OpenClaw's BlueBubbles channel to silently drop inbound messages older than a configurable threshold, preventing processing of stale webhooks and internal error replies.

๐Ÿ” Symptoms

Observed Behavior

When BlueBubbles re-delivers old message webhooks, the following symptoms occur:

  • Stale Message Processing: Messages with timestamps hours or days in the past appear in the agent's conversation flow as if they are new
  • Erroneous Replies: The agent generates contextual replies to ancient messages, confusing users
  • Rate-Limit Error Replies: When the AI service returns a 529 (rate-limit) response, the raw error text is sent back as an iMessage reply:
    The AI service is temporarily overloaded. Please try again in a moment.

Technical Manifestations

The problematic webhook payloads contain dateCreated values that do not align with the current time:

json { “id”: “msg-abc123”, “dateCreated”: “2026-01-15T08:30:00Z”, “text”: “Hello from yesterday”, “guid”: “some-guid” }

When the current server time is 2026-02-24T14:00:00Z, this message is 40 days old but still gets processed through the standard inbound handler pipeline.

๐Ÿง  Root Cause

Architectural Issue

The BlueBubbles inbound handler lacks temporal validation at the message acceptance layer. The webhook processing pipeline has two critical gaps:

  1. No Age Threshold Validation: The handler processes any inbound message regardless of its `dateCreated` timestamp. BlueBubbles occasionally re-delivers messages from its internal queue, sending payloads with timestamps far in the past as if they are fresh incoming messages.
  2. Internal Error Message Leakage: Error messages generated by the internal error handling middleware (particularly 529 rate-limit responses) are being routed through the same reply pipeline as legitimate user messages, causing raw error text to be sent as iMessage replies.

Failure Sequence

  1. BlueBubbles detects an old message in its queue
  2. BlueBubbles sends webhook POST to OpenClaw inbound endpoint
  3. Handler extracts message, extracts dateCreated: “2026-01-15T08:30:00Z”
  4. NO age check performed โ†’ message enters agent processing pipeline
  5. Agent generates response based on stale context
  6. Response sent via BlueBubbles reply channel as iMessage

OR (for error case):

  1. AI service returns 529 rate-limit error
  2. Error middleware generates human-readable error text
  3. Error text flows through reply channel instead of being logged
  4. User receives “The AI service is temporarily overloaded…” as a text message

Code Path Analysis

The issue resides in the message acceptance logic within the BlueBubbles handler. Without a configurable maxMessageAgeSec field, there is no mechanism to:

  • Compare the message's `dateCreated` against the current server time
  • Determine if the elapsed time exceeds a configured threshold
  • Short-circuit the processing pipeline with a silent drop (logged at DEBUG level)

๐Ÿ› ๏ธ Step-by-Step Fix

Configuration Addition

Add the maxMessageAgeSec field to your OpenClaw configuration file (config.json or environment-based config):

json { “channels”: { “bluebubbles”: { “serverUrl”: “https://your-bluebubbles-server.local”, “password”: “your-password-here”, “maxMessageAgeSec”: 300 } } }

Before vs After Configuration

Before (no age filtering):

json { “channels”: { “bluebubbles”: { “serverUrl”: “https://bb-server.local”, “password”: “secret123” } } }

After (with age filtering):

json { “channels”: { “bluebubbles”: { “serverUrl”: “https://bb-server.local”, “password”: “secret123”, “maxMessageAgeSec”: 300 } } }

Handler Implementation Steps

To implement this fix in the codebase, perform the following modifications:

  1. Define the validation constant in the channel config interface:
    // Within channels/bluebubbles/types.ts or similar
    export interface BlueBubblesConfig {
      serverUrl: string;
      password: string;
      maxMessageAgeSec?: number;  // Optional, defaults to no filtering
    }
  2. Add age validation in the inbound message handler:
    // Within the webhook handler function
    async function handleInboundMessage(payload: BlueBubblesWebhookPayload): Promise<void> {
      const config = getBlueBubblesConfig();
    

    // Validate message age if (config.maxMessageAgeSec) { const messageAge = Date.now() - new Date(payload.dateCreated).getTime(); const maxAgeMs = config.maxMessageAgeSec * 1000;

    if (messageAge > maxAgeMs) {
      logger.debug(`Dropping stale message ${payload.guid} (age: ${Math.floor(messageAge/60000)}m)`);
      return;  // Silent drop
    }
    

    }

    // Continue with normal processing… await processMessage(payload); }

  3. Ensure error messages are never routed to reply surfaces:
    // Error middleware or handler
    function handleAIErrors(error: Error, context: MessageContext): void {
      if (error.statusCode === 529) {
        // Rate limit: log internally, do NOT send to user
        logger.warn(`AI service rate-limited for message ${context.messageId}`);
        return;  // No reply sent
      }
    

    // For other errors, decide based on error visibility config if (shouldExposeErrors()) { sendReply(context, generateSafeErrorMessage(error)); } else { logger.error(error); } }

๐Ÿงช Verification

Test Procedure for Message Age Filtering

  1. Deploy the updated configuration and restart OpenClaw service:
    # Restart OpenClaw to load new configuration
    sudo systemctl restart openclaw
    

    Check service status

    sudo systemctl status openclaw –no-pager

  2. Verify configuration is loaded:
    # Check logs for config load
    tail -50 /var/log/openclaw/openclaw.log | grep -i "bluebubbles\|maxMessageAge"
    

    Expected output:

    [INFO] BlueBubbles channel initialized with maxMessageAgeSec=300

  3. Test stale message handling:
    # Send a test webhook with old timestamp via curl
    curl -X POST http://localhost:3000/webhooks/bluebubbles \
      -H "Content-Type: application/json" \
      -H "X-BlueBubbles-Password: your-password" \
      -d '{
        "id": "test-stale-001",
        "guid": "test-stale-guid",
        "dateCreated": "2026-01-15T08:30:00Z",
        "text": "Test stale message"
      }'
    

    Expected: 200 OK, no reply sent, log entry for dropped message

    Check logs for dropped message confirmation

    tail -20 /var/log/openclaw/openclaw.log | grep “Dropping stale”

  4. Test fresh message handling (sanity check):
    # Send a webhook with current timestamp
    curl -X POST http://localhost:3000/webhooks/bluebubbles \
      -H "Content-Type: application/json" \
      -H "X-BlueBubbles-Password: your-password" \
      -d "{
        \"id\": \"test-fresh-001\",
        \"guid\": \"test-fresh-guid-$(date +%s)\",
        \"dateCreated\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",
        \"text\": \"Test fresh message\"
      }"
    

    Expected: 200 OK, message processed normally, iMessage reply sent

Expected Outcomes

  • Stale messages (older than `maxMessageAgeSec`) return HTTP 200 but produce a DEBUG-level log entry; no reply is sent
  • Fresh messages are processed through the normal pipeline and receive agent responses
  • Error messages (529 rate-limit) are logged but never forwarded as iMessage replies

โš ๏ธ Common Pitfalls

  • Timezone Mismatch: Ensure the OpenClaw server's system clock is synchronized with BlueBubbles. If servers are in different timezones and use local time for `dateCreated`, messages may be incorrectly classified as stale or fresh. Use UTC timestamps consistently.
  • Configuration Not Restarted: Edits to `config.json` do not take effect until OpenClaw is restarted. Hot-reload is not supported for structural config changes.
  • Conflicting Overload Behavior: On macOS environments, the `launchctl` service management may not use `systemctl`. Use `launchctl unload` / `launchctl load` instead of `systemctl restart`.
  • Default Behavior Without Config: If `maxMessageAgeSec` is omitted, all messages (including stale ones) are processed. There is no built-in default; you must explicitly set the value.
  • Docker Environment Variable Mapping: When using Docker, nested JSON in environment variables must use double underscores for hierarchy:
    CHANNELS__BLUEBUBBLES__MAXMESSAGEAGESEC=300
  • Log Level Verbosity: DEBUG-level drop messages may not appear in default log output. Ensure your logging configuration is set to DEBUG for the bluebubbles channel, or check the trace log file.
  • 529 Rate Limit Errors: AI service temporarily overloaded responses that leak as iMessage text when error handling is not isolated from reply pipeline
  • EAI_AGAIN / EHOSTUNREACH: Network errors when BlueBubbles server is unreachable; these are distinct from message age issues but may appear in similar logs
  • Duplicate Message Processing: BlueBubbles occasionally sends the same webhook multiple times; when combined with missing age filtering, this causes duplicate agent responses
  • Authentication Failures (401): Incorrect password in config can cause all webhooks to be rejected; unrelated to age filtering but may be mistaken for configuration errors
  • Timestamp Parse Errors: Malformed `dateCreated` fields in BlueBubbles payloads may cause exceptions; ensure graceful handling of invalid date formats

Evidence & Sources

This troubleshooting guide was automatically synthesized by the FixClaw Intelligence Pipeline from community discussions.