How does a complete customer interaction travel through the framework?
This document follows a single conversation from creation to completion while explaining how different framework components cooperate to simulate realistic production behaviour.
Performance testing often focuses on individual requests.
Login
↓
Search
↓
Logout
While this approach is perfectly suitable for stateless APIs, it falls short when the system under test contains asynchronous communication, stateful workflows and multiple interacting users.
This framework models the complete business processes instead.
Instead of asking
"How fast is this endpoint?"
it asks
"How does an entire e2e flow behave under load?"
sequenceDiagram
actor CV as Client VU
participant NR as Node-RED<br/>(webhook)
participant App as Application
participant PG as Postgres
participant Redis
participant Agent as Agent VU
participant WS as Agent<br/>WebSocket
Note over CV,WS: Setup Phase
CV->>NR: POST /webhook (create conversation)
NR->>App: Process message
App->>PG: INSERT conversation
App->>Redis: PUBLISH customer_msg
Note over CV,WS: Agent Assignment
CV->>PG: Poll: SELECT conversation WHERE assigned_agent_id
PG-->>CV: (wait, retry every 5s)
App->>PG: UPDATE conversation (assign agent)
CV->>PG: Poll returns agent_id
Note over CV,WS: Agent Login (parallel)
Agent->>App: GET /login_page
App-->>Agent: HTML
Agent->>App: POST /signin (credentials)
App-->>Agent: Set-Cookie, Bearer token
Agent->>App: GET /main (dashboard)
App-->>Agent: Dashboard HTML
Agent->>Redis: RPUSH ws:queue:pairing:ws {token, jwtId, agentId}
Note over CV,WS: Agent WS Connection
WS->>Redis: BLPOP ws:queue:pairing:ws (wait for token)
Redis-->>WS: {token, jwtId, agentId}
WS->>App: WebSocket handshake (jwtId + token)
App-->>WS: WebSocket established
WS->>Redis: RPUSH ws:queue:pairing:stream agentId
Note over CV,WS: Message Exchange Cycle 1
CV->>App: POST /bot_entry_logic_msg
App->>PG: INSERT message
App->>Redis: PUBLISH new_message
Agent->>App: GET /get_my_conversations
App-->>Agent: conversation_list
Agent->>App: GET /get_conversation_messages
App-->>Agent: message_list (includes new message)
Note over CV,WS: Agent Sees Message (WS notification)
App->>WS: WebSocket push (new_message_notification)
WS->>Redis: Record actual_event with timestamp
Agent->>App: POST /agent_conversation_msg (reply)
App->>PG: INSERT agent_reply
App->>Redis: PUBLISH agent_reply
Agent->>App: PUT /agent_conversation_update_last_seen
Note over CV,WS: Metrics Recording
CV->>Redis: INCR client_vu_number
Agent->>Redis: INCR agent_vu_number
WS->>Redis: Compare expected vs actual event timestamps
Redis->>InfluxDB: Push agents_ws_event_latency (ms)
App->>InfluxDB: Push http_req_duration (ms)
PG->>InfluxDB: Push db_query_duration (ms)
Note over CV,WS: Think Time (8s delay)
CV->>CV: sleep(8000)
Agent->>Agent: sleep(8000)
Note over CV,WS: Repeat for CONV_MSG_CYCLES times (default 6)
From the diagram it is easy to see that this is not a traditional HTTP benchmarks flow.
This framework models an entire customer support interaction involving multiple independent virtual users.
A single business workflow consists of six major phases:
- Setup Phase – a Client creates a new conversation through the webhook while the Agent authenticates in parallel.
- Agent Assignment – the Client waits until the application assigns an available Agent.
- WebSocket Pairing – the Agent publishes authentication data to Redis and a dedicated WebSocket Virtual User establishes a persistent connection.
- Message Exchange – Client and Agent exchange messages while WebSocket notifications are delivered asynchronously.
- Metrics Recording – workload scenarios, monitors and the WebSocket Event Checker record application, infrastructure and business metrics.
- Conversation Lifecycle – both actors complete the conversation, perform post-processing tasks and continue participating in the workload until the test ends.
Although these phases appear sequential in the diagram, several of them execute concurrently throughout the test.
The workflow begins with the Client Virtual User creating a new conversation.
Instead of communicating directly with the application, the Client first submits the initial message through a Node-RED webhook.
Node-RED works as connector with its own internal logic.
This logic cannot be influenced or interacted with from outside.
The application processes the webhook, creates the conversation and other entities in PostgreSQL and publishes an internal event announcing that a new customer message has arrived.
At the same time, Agent Virtual Users perform their own initialization:
- open the login page
- authenticate
- enter the dashboard
- receive an authentication token and JWT identifier
After a successful login, the Agent stores its authentication information inside a Redis pairing queue.
This allows the WebSocket Virtual User to establish the connection independently from the HTTP workflow.
The Agent begins polling application until an Agent has been assigned.
The assignment is performed asynchronously by the application itself.
Neither participant knows when the assignment will happen.
Client does not know which Agent or when he will be assigned.
Agent does not know which conversation or when it will be assigned.
In a real contact center, the Agent repeatedly checks the dashboard:
- "Has a new conversation been assigned to me?"
- if no:
- wait
- check dashboard again
- if yes:
- opens the assigned conversation
- reviews the customer's information and message history
- takes conversation into work
This waiting period belongs to the business workflow rather than the transport layer.
Even when every HTTP request is fast, conversation assignment may still become a bottleneck due to routing logic, Agent availability or application processing.
The framework not only reproduces this behaviour but also evaluates its efficiency through Meta Metrics.
These metrics describe the business workflow itself rather than individual requests and help identify bottlenecks that would remain invisible when analysing HTTP latency alone.
Metrics collected during this phase include:
- Agent utilization
- idle conversation count
- successful assignments
- assignment failures
graph TB
subgraph AgentVU["Agent VU"]
ALive["Agent Active"]
AMsg["Send Message"]
AEvent["Record Event:<br/>ws:stream:event:agentId"]
end
subgraph WSVu["WS Listener VU"]
WSWait["Wait for Token"]
WSOpen["Open WebSocket"]
WSRecv["Receive Notification"]
WSEvent["Record Event:<br/>ws:queue:event:agentId"]
end
subgraph Monitor["Event Stream Checker VU"]
Check["Compare Events"]
Calc["Calculate Latency"]
Record["Record Success/Error"]
end
subgraph Redis["Redis"]
Queue1["ws:queue:pairing:ws<br/>(LIST)"]
Stream["ws:stream:event:agentId<br/>(STREAM)"]
Queue2["ws:queue:event:agentId<br/>(LIST)"]
Queue3["ws:queue:pairing:stream<br/>(LIST)"]
end
ALive -->|1. Login| Queue1
ALive -->|Push Token| Queue1
Queue1 -->|2. BLPOP Token| WSWait
WSWait -->|Extract Token| WSOpen
WSOpen -->|3. Open WS| WSRecv
AMsg -->|4. Send Message<br/>+ Timestamp| Stream
WSRecv -->|5. Receive on WS<br/>+ Timestamp| WSEvent
WSEvent -->|Push Event| Queue2
Queue2 -->|6. Read Actual| Check
Stream -->|7. Read Expected| Check
Check -->|8. Compare| Calc
Calc -->|9. Latency| Record
Record -->|10. Record Metrics| Monitor
WSOpen -->|11. Push AgentId| Queue3
style AgentVU fill:#E8F5E9,stroke:#333
style WSVu fill:#F3E5F5,stroke:#333
style Monitor fill:#FFF3E0,stroke:#333
style Redis fill:#E1F5FE,stroke:#333
The Agent Virtual User never opens a WebSocket connection itself.
Instead, after login it publishes its authentication data into a Redis queue.
The dedicated WebSocket Virtual User blocks on that queue, retrieves the authentication payload and establishes the WebSocket session independently.
Once connected, the WebSocket scenario publishes the Agent identifier into another Redis queue so that monitoring scenarios know which WebSocket streams should be observed.
Keeping HTTP and WebSocket responsibilities in separate scenarios provides several advantages:
- cleaner separation of concerns
- independent reconnect logic
- isolated WebSocket latency measurements
- easier debugging of asynchronous communication
Moreover, this does tackle one of the k6 limitations: the VU scenario is sequentual.
This is not a JMeter where you can casually spawn a branching parallel thread.
After both participants are ready, the conversation enters its main workload loop.
Each iteration consists of:
- Client sends a new message.
- Application stores the message.
- Agent periodically refreshes conversation data.
- WebSocket pushes a notification announcing the new message.
- Agent sends a reply.
- Application stores the reply.
- Conversation state is updated.
Each real action - message sending - does introduce a think time. Different and configurable for each actor.
A human being cannot formulate, write and send message in span of miliseconds.
We require time to read incoming messages, check documents, decide on the appropriate response, etc.
Iteration repeats for the configured CONV_MSG_CYCLES value before the scenario completes.
Unlike synthetic request loops, every iteration represents one complete and realistic business interaction.
While the business workflow executes, several independent components collect different categories of metrics.
The Client and Agent scenarios record:
- HTTP performance
- business transactions
- activity counters
The WebSocket scenario records:
- notification timestamps
- successful deliveries
- connection state
The Event Stream Checker continuously compares expected events written by the Agent with actual WebSocket events received by the listener.
The timestamp difference between those two events becomes the measured WebSocket delivery latency.
At the same time, monitoring scenarios collect infrastructure information including PostgreSQL connection counts and other system-level metrics.
Rather than relying on a single source of truth, the framework correlates metrics produced by multiple independent actors.
Unlike many example k6 scenarios, both the Client and the Agent are long-lived Virtual Users.
Completing a conversation does not terminate the scenario.
Agent doesn't go home after one conversation.
Contact center doeen't have only one Client.
Instead, both actors continue participating in the workload until the configured test duration has elapsed.
This better reflects production behaviour where users continuously interact with the system rather than performing a single isolated action.
At the end of each conversation, both participants perform their own completion workflow.
The Agent completes the business process by:
- sending the final reply
- marking the conversation as Resolved
- becoming available for the next assignment
The Client finishes its interaction by:
- receiving the CSAT (Customer Satisfaction) request
- submitting a satisfaction score
- becoming ready to create the next conversation
Each actor then applies its configured think time before entering the next iteration of the workload.
The complete conversation lifecycle repeats until the configured test duration expires.
For test:
This continuous lifecycle produces a steady-state workload that more closely resembles production environments than fire-once scenarios, where Virtual Users terminate immediately after completing a single business transaction.
For me:
Provides VU traceability where I am in full control.
Real systems rarely behave perfectly.
The framework therefore expects failures rather than assuming success.
Examples include:
- assignment timeout
- failed authentication
- missing WebSocket events
- delayed synchronization
- HTTP retries
- unavailable infrastructure
Whenever possible, failures are reported as metrics rather than immediately terminating execution.
This allows performance reports to explain not only what failed, but also how frequently failures occurred throughout the test.
One question often appears during code reviews:
Why not perform everything inside a single Virtual User?
Let's imagine this is technically possible in this case.
While this approach would reduce the number of scripts, it would also introduce several problems.
A single scenario would become responsible for:
- business logic
- synchronization
- monitoring
- WebSocket management
- reporting
Combining unrelated responsibilities quickly leads to large, difficult-to-maintain scripts.
Instead, every scenario performs one clearly defined role while Redis coordinates collaboration between them.
This design follows the same separation-of-concerns principles commonly applied in production software.
The biggest lesson that implementing realistic workflows can provide: business processes are significantly more valuable than isolated requests.
Endpoints rarely operate independently in production.
Users authenticate, wait for assignments, receive notifications, exchange messages and interact asynchronously.
Modelling those interactions required more engineering effort, but the resulting workloads behave much closer to real production traffic.
The additional complexity also made performance investigations considerably more meaningful because metrics now describe complete user journeys rather than disconnected HTTP requests.
Dear reader remember one thing: "endpoint is holding 100k RPS" is useless if production does require 100