Skip to main content

Command Palette

Search for a command to run...

Why Sending a WebSocket Reply From Serverless Feels Weird

Updated
5 min readView as Markdown
C

Cortlet is a software brand specailizing in: Lightweight, secure-first, no-bloat, problem-solving, and balance of opensource and closed source software.

With a normal HTTP API, the response path is obvious.

A client sends a request.

The server processes it.

The server returns a response.

That mental model is so familiar that developers barely think about it.

WebSockets on serverless infrastructure can feel very different.

A client sends a message over a WebSocket, the gateway triggers a serverless function, and then the developer discovers something strange:

Returning JSON from the function does not always mean that JSON goes back to the WebSocket client.

That is where the architecture starts feeling less intuitive.

HTTP has a natural request-response flow

A normal HTTP interaction looks like this:

Client → HTTP Request → Serverless Function → HTTP Response → Client

The response already knows where it needs to go.

The function returns data, the platform sends it back to the caller, and the interaction is finished.

WebSockets are different because the connection often lives somewhere outside the serverless function itself.

A simplified architecture might look like:

Client → WebSocket Gateway → Serverless Function

The gateway owns the persistent socket.

The function only receives an event.

That means the function does not necessarily own the response channel.

The return value may not be the socket response

Imagine a client sends:

{"action":"ping"}

The gateway forwards that event to a serverless function.

The function processes it and returns:

{"message":"pong"}

It would be reasonable to expect that response to travel back to the same client.

But in many serverless WebSocket setups, that is not the default behavior.

The function return value may simply indicate that the backend invocation completed successfully.

To actually send data back over the WebSocket, the backend may need to make a separate call to the gateway.

Conceptually, the flow becomes:

Client → WebSocket Gateway → Function

and then:

Function → Gateway Management API → Client

That is very different from ordinary HTTP.

Now the backend needs to know the connection

To send that second request, the backend usually needs some form of connection identifier.

That means the function may need to know:

  • which connection sent the original message

  • whether that connection is still active

  • which gateway endpoint should receive the callback

  • how to authenticate the callback request

  • what to do if the connection disappeared before the response arrives

The response path has now become its own piece of infrastructure.

A simple:

receive message → process → return response

can turn into:

receive event → extract connection ID → process event → initialize gateway client → send callback → handle stale connection

None of those steps are impossible.

They are just additional machinery.

Why serverless makes this more noticeable

On a traditional WebSocket server, the active socket object is often already available in memory.

Conceptually, the application can do something like:

socket.send(response)

The same process that received the message still owns the connection.

With serverless, that assumption disappears.

The function processing the event may have no direct access to the underlying WebSocket at all.

The connection belongs to another layer.

That separation is useful for scaling, but it changes the developer experience.

Async workloads make it even more complicated

The response path becomes even more important when the work cannot finish immediately.

Suppose a WebSocket message starts a longer-running job.

The initial function might place the work onto a queue and exit.

Later, another worker finishes the job.

Now that worker needs to send the result back to the original client.

The architecture might become:

Client → WebSocket Gateway → Function → Queue → Worker

followed by:

Worker → Gateway API → WebSocket Client

At that point, the application needs some way to preserve the relationship between the original connection and the eventual result.

Again, the application starts managing transport infrastructure alongside its actual business logic.

What if the webhook response could become the socket response?

There is another possible model.

Imagine this:

Client → WebSocket → Gateway → HTTP Webhook

The backend receives an ordinary HTTP request.

Then it simply returns:

200 OK

with:

{"message":"pong"}

The gateway takes that HTTP response and automatically sends it back over the originating WebSocket connection.

The developer gets a familiar programming model:

WebSocket message → HTTP request → HTTP response → WebSocket message

The persistent connection still exists.

But the backend does not need a special SDK or a second callback request for a basic response.

This is one of the ideas behind Cortlet

With Cortlet, we are exploring whether WebSocket infrastructure can feel more like ordinary HTTP infrastructure.

The gateway would own the persistent connection.

The backend would receive standard HTTP webhooks.

For simple request-response interactions, the HTTP response could potentially become the WebSocket response automatically.

More complex asynchronous flows could still use explicit outbound APIs when needed.

The goal is not to hide how WebSockets work.

It is to remove infrastructure steps that the application should not need to repeat for every message.

Familiar interfaces matter

Developers already understand HTTP extremely well.

Requests come in.

Responses go out.

Serverless platforms are designed around that model.

If a WebSocket gateway can translate persistent connections into that familiar request-response flow, realtime applications may require less WebSocket-specific backend code.

And that leads to a useful question:

If the backend already knows how to return an HTTP response, why should sending a basic WebSocket reply require an entirely separate code path?