Why this framework uses Redis as a synchronization layer instead of relying on native k6 mechanisms.
One of the first architectural challenges I encountered during the migration from JMeter to k6 was surprisingly simple:
How can two independent virtual users exchange information?
In this project, virtual users do not work in isolation.
A customer creates conversations.
Agents authenticate.
WebSocket listeners wait for notifications.
Database monitors observe state changes.
All of these components need to exchange information while remaining independent.
Unfortunately, k6 virtual users intentionally do not share memory.
This design improves isolation and scalability, but it also means that implementing realistic business workflows requires another synchronization mechanism.
In the previous version of this framework (implemented in JMeter), synchronization relied heavily on JMeter Properties (props).
Properties provide a JVM-wide shared storage that every thread can access.
For example:
- Customer thread creates a conversation.
- Conversation ID is written into
props. - Agent thread reads the ID.
- WebSocket listener retrieves the same information.
The approach worked well because all threads executed inside the same JVM.
Migrating to k6 meant losing that shared-memory model.
So, different solution became necessary.
Before choosing a synchronization mechanism, I defined several requirements.
The solution needed to:
- exchange information between independent virtual users
- support asynchronous workflows
- preserve ordering where necessary
- avoid race conditions
- remain lightweight
- scale together with the load test
- require minimal changes to scenario code
Only after defining these requirements did I begin evaluating possible approaches.
SharedArray is one of the first synchronization mechanisms many new k6 users discover.
At first glance it appears promising because all virtual users can access the same object.
Unfortunately, SharedArray is intentionally read-only.
Its purpose is efficient sharing of static datasets.
Typical examples include:
- CSV files
- JSON configuration
- user credentials
It cannot be modified during test execution.
For this project, where conversations, tokens and assignments are continuously created, it simply cannot solve the synchronization problem.
❌ Suitable for static data.
❌ Cannot synchronize runtime state.
Another possibility was using setup().
The setup phase executes once before virtual users start running.
It is extremely useful for:
- authentication
- preparing datasets
- creating test users
- generating initial configuration
However, it has one major limitation.
Once execution begins, the returned data becomes immutable.
Virtual users cannot update it.
For stateful business workflows, synchronization must happen continuously throughout the test, not only before it begins.
✅ Excellent for initialization.
❌ Cannot synchronize runtime events.
Another option was storing synchronization data inside a relational database.
This would certainly work.
Databases naturally support:
- concurrent reads
- concurrent writes
- persistence
- transactions
However, they also introduce unnecessary complexity.
The synchronization layer in this framework stores highly transient information:
- conversation IDs
- authentication tokens
- assignments
- timestamps
Persisting these values permanently provides little benefit.
Database queries would also introduce additional latency and increase infrastructure requirements.
✅ Technically feasible.
Redis turned out to match the requirements perfectly well.
Redis provides exactly the primitives needed for synchronization.
Depending on the scenario, different data structures are appropriate:
- Lists for work queues
- Streams for ordered event processing
- Hashes for shared state
- Sets for uniqueness checks
- Expiration for automatic cleanup
Redis acts as a lightweight coordination layer rather than a permanent storage system.
Because all virtual users communicate through Redis, each scenario remains independent while still participating in the same business workflow.
Choosing Redis introduced another infrastructure dependency.
The framework now requires:
- Redis
- InfluxDB
- Reporting Service
instead of only running k6.
That increases deployment complexity.
However, the architectural benefits significantly outweigh this cost.
Redis made it possible to implement:
- stateful business workflows
- asynchronous communication
- agent/customer matching
- WebSocket coordination
- event-driven synchronization
without introducing tight coupling between scenarios.
A common question is why not simply store everything inside global JavaScript objects.
The answer is straightforward.
Every k6 virtual user runs inside its own isolated JavaScript runtime.
Global variables are not shared.
Changing a variable in one virtual user has no effect on another.
This isolation is one of the core architectural principles of k6.
Any synchronization mechanism must therefore exist outside the JavaScript runtime.
Initially, I approached the migration expecting to replace JMeter Properties with another in-memory mechanism.
That assumption quickly proved incorrect.
Instead of searching for shared memory, I started thinking in terms of distributed systems.
Once that shift happened, Redis became an obvious fit.
Rather than sharing memory, virtual users exchange messages through an explicit coordination layer.
This approach scales better, is easier to reason about, and more closely resembles how modern distributed applications communicate.