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:
- 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.
- 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
- BlueBubbles detects an old message in its queue
- BlueBubbles sends webhook POST to OpenClaw inbound endpoint
- Handler extracts message, extracts dateCreated: “2026-01-15T08:30:00Z”
- NO age check performed → message enters agent processing pipeline
- Agent generates response based on stale context
- Response sent via BlueBubbles reply channel as iMessage
OR (for error case):
- AI service returns 529 rate-limit error
- Error middleware generates human-readable error text
- Error text flows through reply channel instead of being logged
- 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:
- 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 } - 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); }
- 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
- Deploy the updated configuration and restart OpenClaw service:
# Restart OpenClaw to load new configuration sudo systemctl restart openclawCheck service status
sudo systemctl status openclaw –no-pager
- 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
- 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”
- 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.
🔗 Related Errors
- 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