Skip to content

Asynchronous communication

An asynchronous request sends a message and immediately returns without waiting for a response. This method is ideal for high-throughput scenarios such as logging or data publishing, as well as for commands that do not require a result.

To send an asynchronous message, apply it to a negative connection handle (for example, use the neg operator on the handle).

// Asynchronously instruct the server to create a variable 'a'
q)neg[h]"a:10"

// Asynchronously instruct the server to print "hello from client" to its console
q)neg[h]("0N!";"hello from client")

Because the client does not wait for a response, asynchronous messaging is critical in systems like tickerplants, where blocking for a slow subscriber is unacceptable.

Performance optimization

To improve performance, consider increasing the TCP send/receive buffer sizes on your operating system.

The operating system reserves this buffer memory per socket, whether or not that socket is currently busy. Scaling the buffer size up across thousands of concurrent connections can deplete system memory rather than improve throughput, so size it for the connection count you actually expect, not just the busiest one.

Flush the message queue

An asynchronous message optimizes for unblocking the sender, not for request latency. By default, q buffers the message in user space and writes it to the socket in its own time. An async flush forces q to hand the serialized bytes for a handle over to the kernel immediately.

// Flush all pending async messages on handle h
q)neg[h][]
// or equivalently:
q)neg[h](::)

You can inspect the size of the outbound queue for all handles using .z.W or for a specific handle with -38!x.

Flushing and confirmation

Sending any synchronous message on the same handle (for example, h"") also flushes the asynchronous queue. Because messages are processed in order, receiving a response to that message confirms that the remote end has processed all prior asynchronous messages.

Broadcast a message

To send the same message to multiple clients, use the async broadcast (-25!) function. This approach is more efficient than sending the message to each client in a loop, because the message is serialized only once.

// Handles for three connected clients
q)h1:3; h2:4; h3:5;

// Serialize 'msg' once and send it to handles h1, h2, and h3
q)msg:(`someFunction; `arg1; `arg2);
q)-25!((h1;h2;h3); msg)

Deferred sync

Although asynchronous messages do not block the sender, the receiving process can be programmed to send a response back. The original sender can then block and wait specifically for that response.

To block and wait for the next incoming message on handle h, call h[].

// On the client:
// 1. Send an asynchronous request
q)neg[h] (`someAsyncFunction; 1; 2);

// 2. Block and wait for a message to arrive on the same handle
//    This assumes the server is programmed to send a message back on this handle
q)response: h[];

This pattern combines asynchronous and synchronous behavior. The client sends an asynchronous request, performs other work, and then blocks only when it needs the final result.

  1. Server setup:

    q)\p 5000
    q)add: {x+y+z}
    
    // This function receives an async request, computes a result,
    // and sends it back to the original client handle (.z.w) asynchronously
    q)proc: {neg[.z.w] (add . x)}
    
  2. Run the client:

    q)h: hopen `::5000
    
    // 1. Send an async request to run 'proc' on the server
    //    Execution continues immediately on the client
    q)neg[h](`proc; 1 2 3);
    
    // ... client can perform other tasks here ...
    
    // 2. Now, block and wait for the response on handle 'h'
    q)res:h[];
    q)res
    6
    

Asynchronous callbacks

An asynchronous callback lets a client send a request without blocking, and still receive the result once the server has it: the server replies by making its own asynchronous call back to a function the client names in the request. Because neither the request nor the reply blocks, the client is free to do other work while it waits, and one client's slow or unresponsive callback cannot stall the server's response to anyone else.

Asynchronous callbacks require coordinating three components: the client's call, the server function's signature, and the server's callback mechanism.

  1. Client-side call: The client initiates an asynchronous request to a named function on the server. The last argument in the call is a symbol representing the name of the callback function that exists on the client.

    / Syntax: (neg h) (`remoteFunc; arg1; ..; argN; `clientCallback)
    (neg h) (`proc; 42; `clientFunc)
    
  2. Server-side function signature: You must define the remote function on the server to accept the callback function's name as its final argument.

    / Syntax: remoteFunc:{[arg1; ..; argN; callbackName] ... }
    proc:{[dataArg; callbackName] ... }
    
  3. Server-side callback execution: Inside the remote function, use .z.w to get the connection handle of the calling process. Use this handle to make an asynchronous callback to the client's specified callback function, passing the result.

    proc:{[dataArg; callbackName]
      result: dataArg * 2;               / Process the data
      h: .z.w;                           / Get caller's handle
      (neg h) (callbackName; result);    / Execute the callback
    }
    

Notice in the above implementation:

  • .z.w must be called inside the server function to get the connection handle of the calling process
  • The callback must be asynchronous (neg h) to ensure non-blocking communication

Basic callback example

The simplest case is where a client calls a remote function with a single argument and provides a callback to receive the result.

The following examples use 0N! to print output to the console in the q session.

On the server process: Define a remote function proc that accepts a data argument x and a callback name y. It performs an operation and then uses the caller's handle (.z.w) to invoke the callback.

\p 5000                                           / Listen on port 5000
serverFunc:{0N!x;}                                / Represents the core server logic
proc:{serverFunc x; h:.z.w; (neg h) (y; 43)}      / Function for client to call

On the client process: Define the callback function clientFunc, open a handle to the server, and make the asynchronous call to run proc.

clientFunc:{0N!x;}                                / Callback handler for result
h:hopen `::5000
(neg h) (`proc; 42; `clientFunc)

Result: The server console displays 42, and the client console then displays 43.

Handle multiple parameters

To call a remote function with multiple data arguments, pass them as a list.

On the server process: Define proc3 to accept a list of arguments x and a callback name y. It uses Apply (.) to pass the list of arguments to the target function add3.

\p 5000
add3:{x+y+z}                                      / A function that takes 3 arguments
proc3:{r:add3 . x; 0N!r; (neg .z.w) (y; r)}       / Wrapper to handle arg list and callback

On the client process: The client sends the arguments as a single list.

clientFunc:{0N!x;}
h:hopen `::5000
(neg h) (`proc3; 1 2 3; `clientFunc)

Result: The server console displays 6, and the client console then displays 6.

Generic function wrapper

To avoid writing a custom wrapper for every server-side function, you can implement a single generic function wrapper. This allows a client to run any server-side function without the need to write additional code. This example refers to the generic function wrapper as the dispatcher function.

On the server process:

The dispatcher function takes three arguments:

  • The target function's name
  • The target function's arguments as a list
  • The client callback name

It dynamically runs the target function and returns the result.

\p 5000
add3:{x+y+z}                                  / An arbitrary server function
dispatcher:{(neg .z.w) (z; (value x) . y)}    / Wrapper to run non-unary functions

On the client process: The client calls dispatcher, specifying the target function (add3), its arguments (1 2 3), and the callback function (clientFunc).

clientFunc:{0N!x;}
h:hopen `::5000
(neg h) (`dispatcher; `add3; 1 2 3; `clientFunc)

Result: The client console displays 6. The server performs the calculation, but no output appears there.

Remote execution with anonymous functions

Send a function literal (an anonymous function) from the client to run directly on the server. This technique requires no predefined functions on the server.

On the server process: Start a q process listening on a port. You don't need function definitions.

$ q -p 5000

On the client process: The client sends an anonymous function that encapsulates both the logic to run and the callback invocation.

q)clientFunc:{0N!x;}
q)h:hopen `::5000
q)(neg h) ({(neg .z.w) (z; x*y)}; 6; 7; `clientFunc)
42

The client's asynchronous message sends:

  • An anonymous function: {(neg .z.w) (z; x*y)}
  • Its arguments: 6 and 7
  • The name of the callback function: `clientFunc

Result: The multiplication 6*7 occurs on the server, and the client console displays the result 42.

Security warning

Use this IPC pattern with extreme caution in a production environment. An unprotected server is vulnerable to arbitrary code execution from any client. Protect your server by authorizing which functions can run.

Prevent deadlock

When implementing callbacks, you must use asynchronous calls for both the initial request from the client and the callback from the server. Failure to do so results in deadlock.

If a client makes a synchronous call to the server, it blocks and waits for a response. If the server function then attempts to make a callback (even an asynchronous one) to the client, the client cannot process it because it is still blocked by its original synchronous call. The system hangs indefinitely.

Always use the negative handle form (neg h) for both the outgoing request and the callback invocation to ensure non-blocking communication in both directions.