Fix RelayHook concurrent-call responses with UUID request IDs
Fix RelayHook concurrent calls: generate a UUID request ID per webhook call, store it in request context, and route responses by ID to prevent mixups.
TABLE OF CONTENTS
If RelayHook (or any webhook relay / request-response proxy) mixes up responses when two calls happen at the same time, the fix is almost always the same: create a unique request ID for every inbound request and carry it through every hop until you send the response back. In practice, that means generating a UUID, storing it with the request context, and refusing to “guess” which browser tab/session should receive a response.
What’s happening (the bug pattern)
When a relay tool is doing synchronous request/response handling in a browser (or a browser-adjacent runtime), concurrent requests can collide in ways that look like:
- Request A comes in on Tab 1, Request B comes in on Tab 2
- The relay processes both concurrently
- The response intended for Request A gets delivered to Tab 2 (or vice versa)
This is typically caused by one of these implementation issues:
- Shared global state (e.g., “current response handler”) that gets overwritten by the second request
- Tab/session reuse where multiple calls are being mapped to the same “active tab” context
- Race conditions in async code, where “the last listener wins”
- Missing correlation key between the inbound request and the outbound response
The practical fix: UUID per request (and thread it everywhere)
Step 1: Generate a request ID at the edge
The moment the request enters your system, assign it a unique ID.
- Prefer UUIDv4 (random) unless you have an existing deterministic ID from the upstream provider.
- Treat this as a correlation ID for the whole lifecycle.
Example (pseudo-code):
requestId = uuid()
Step 2: Attach the ID to the request context
Store it in the same structure you use to track the call, for example:
- in-memory map keyed by requestId
- per-tab or per-session store (but still keyed by requestId)
- persisted store if you need recoverability
The key requirement: never use “tab” alone as the identifier when more than one request can be in-flight.
Step 3: Propagate the request ID through every hop
Thread it through:
- logs
- any internal events/messages
- downstream calls (headers, query params, payload fields)
- the response routing layer
Common patterns:
X-Request-Id: <uuid>headerrequest_idfield in the JSON payload
Step 4: Route the response by request ID, not by tab
When the response is ready, lookup the exact handler/connection using the requestId.
- If the handler is missing (tab closed, timeout, etc.), fail safely and log it.
- If there are multiple handlers (shouldn’t happen), treat it as a bug and do not deliver a response.
Step 5: Add timeouts + cleanup
Every in-flight request should have:
- a timeout window
- cleanup logic to remove it from your map/store
Without cleanup, the system can leak memory and eventually degrade.
Common pitfalls (and how to avoid them)
Pitfall: “Session ID” that’s shared across concurrent calls
If multiple requests share a session ID, you still can’t reliably route concurrent responses. Session IDs are useful, but they’re not granular enough.
Pitfall: One event listener for multiple in-flight calls
If your implementation uses a single “response event” listener that updates on every request, concurrency will break it.
Pitfall: Relying on timing
Anything like “whichever response finishes first goes to the currently active tab” is guaranteed to fail under real-world load.
Testing checklist (to prove it’s fixed)
Use a simple concurrent test plan:
- Send 10–50 concurrent requests to the same hook/endpoint.
- Ensure each request includes a unique request ID (or confirm the relay generates one).
- Return a response body that includes the request ID.
- Validate that every caller receives its own request ID back.
Recommended test assertions:
- No duplicate IDs in responses
- No missing IDs
- No mismatched IDs
- Stable behavior when:
- a tab closes mid-flight
- responses are delayed randomly (simulate jitter)
- one request errors while others succeed
When to consider a different architecture
If you need strict reliability under high concurrency, move response routing out of the browser:
- a lightweight server component that handles correlation and routing
- a queue for processing + immediate 202 response to the sender
For a deeper walkthrough of webhook response constraints and why async automations in tools like Zapier or Pipedream can struggle with strict request/response contracts, see: RelayHook vs Zapier Webhooks: what's broken and how to fix it
Get help debugging your webhook routing
If you're seeing weird concurrency behavior in RelayHook (or any webhook relay) and want a clean, testable pattern for correlation IDs + response routing, book a free consulting call — we can help you design and implement it.