Stateless Architecture
VCR runs multiple replicas of every application, and can scale further under load. Replicas can also be scaled down to zero when idle. This has important implications for how you manage state.
Why In-Memory State Is Unsafe
Any data stored in memory (global variables, module-level objects, in-process caches) is:
- Local to one replica only
- Lost on restart or scale-to-zero
- Invisible to other replicas handling concurrent requests
Never assume that two requests will hit the same replica. If they do today, that will not hold as load increases.
Wrong:
// Replica-local — invisible to other replicas and lost on restart
const cache = {};
cache['userId'] = { name: 'Alice' };
let requestCount = 0;
requestCount++;
Correct:
import { vcr } from '@vonage/vcr-sdk';
const state = vcr.getInstanceState();
await state.set('userId', { name: 'Alice' });
await state.increment('requestCount', 1);
Choosing the Right State Scope
| Scope | Access method | Visibility |
|---|---|---|
| Session | new State(session) | One session only |
| Instance | vcr.getInstanceState() | All replicas of this deployed instance |
| Account | vcr.getAccountState() | All applications in the Vonage account |
vcr.getInstanceState() is the correct default for shared state. It is a convenience wrapper that scopes State to the current instance ID, so all replicas of the same deployment share the same store.
vcr.getAccountState() is for cross-application data, such as a shared blocklist or configuration that multiple VCR apps need to read.
Session state (new State(session)) is appropriate for data that belongs to a single user interaction, such as conversation context or call state.
Common In-Memory Patterns to Replace
| Pattern | Replacement |
|---|---|
const cache = {} at module level | vcr.getInstanceState() |
| Singleton object holding request state | vcr.getInstanceState() or vcr.createSessionWithId(userId) |
global.something = ... | vcr.getInstanceState() |
| Counter incremented per request | state.increment('counter', 1) |
Code Examples
Node.js — shared counter across replicas:
import { vcr } from '@vonage/vcr-sdk';
const state = vcr.getInstanceState();
// Increment atomically — safe across replicas
await state.increment('requestCount', 1);
const count = await state.get('requestCount');
Node.js — per-user session state:
import { vcr, State } from '@vonage/vcr-sdk';
app.post('/message', async (req, res) => {
// Each user gets an isolated state namespace
const session = vcr.createSessionWithId(req.body.userId);
const state = new State(session);
await state.set('lastSeen', new Date().toISOString());
res.sendStatus(200);
});
Python — shared state across replicas:
Session Scoping Best Practices
Critical: Misusing vcr.createSession() is one of the most common sources of bugs in VCR applications. Understanding session scoping is essential for building apps that scale correctly.
The Problem with vcr.createSession() at Global Scope
vcr.createSession() generates a random, ephemeral session ID each time it is called. If you call it at module/global scope and use the resulting session with new State(session):
- Each replica creates its own random session on startup
- Data written by one replica is invisible to all others
- On restart or scale-to-zero, the session ID is lost and the data becomes permanently orphaned
- This completely defeats the purpose of using the State provider for shared data
Anti-pattern — DO NOT do this:
import { vcr, State } from '@vonage/vcr-sdk';
// WRONG: random session at global scope
const session = vcr.createSession();
const state = new State(session);
app.post('/message', async (req, res) => {
// This state is only accessible by this exact replica, using this exact session ID.
// Other replicas have their own random session and cannot see this data.
await state.set('messageCount', (await state.get('messageCount') || 0) + 1);
res.sendStatus(200);
});
Correct Patterns
For shared state across all replicas — use Instance State
import { vcr } from '@vonage/vcr-sdk';
// Shared across all replicas of this instance — no session needed
const state = vcr.getInstanceState();
app.post('/message', async (req, res) => {
await state.increment('messageCount', 1);
res.sendStatus(200);
});
For per-conversation/per-user state — use deterministic session IDs
Scope sessions to request context using IDs from callbacks. This ensures any replica handling a subsequent request for the same conversation/user can access the same state.
import { vcr, State } from '@vonage/vcr-sdk';
// Voice: scope to the conversation UUID from the callback
app.post('/onCall', async (req, res) => {
const session = vcr.createSessionWithId(req.body.conversation_uuid);
const state = new State(session);
await state.set('step', 'greeting');
await state.set('startTime', Date.now());
res.json([{ action: 'talk', text: 'Welcome!' }]);
});
// SMS: scope to the sender's phone number
app.post('/onMessage', async (req, res) => {
const session = vcr.createSessionWithId(req.body.from);
const state = new State(session);
await state.increment('messageCount', 1);
await state.set('lastMessage', req.body.text);
res.sendStatus(200);
});
The key insight: the session ID must be deterministic and derivable from the request context. Good session IDs include:
| Source | Example ID | Use case |
|---|---|---|
| Voice callback | conversation_uuid | Per-call IVR state |
| SMS/MMS callback | sender phone number | Per-sender conversation history |
| Authenticated user | user ID or JWT subject | Per-user preferences |
| Custom correlation | order ID, ticket ID | Per-workflow state |
For subscription registration — use Global Session
vcr.getGlobalSession() returns a deterministic session shared across all replicas. Use it only for registering provider subscriptions (voice, messages), not for storing application state.
import { vcr, Voice, Messages } from '@vonage/vcr-sdk';
// Correct: global session for subscription registration
const globalSession = vcr.getGlobalSession();
const voice = new Voice(globalSession);
const messages = new Messages(globalSession);
await voice.onCall('onCall');
await messages.onMessage('onMessage',
{ type: 'sms', number: process.env.VONAGE_NUMBER },
{ type: 'sms', number: undefined }
);
Summary
| Need | Method | Scope |
|---|---|---|
| Shared counters, caches, config | vcr.getInstanceState() | All replicas |
| Per-user/per-conversation state | vcr.createSessionWithId(id) + new State(session) | Deterministic, request-scoped |
| Cross-app shared data | vcr.getAccountState() | All apps in account |
| Subscription registration | vcr.getGlobalSession() | Instance-wide singleton |
| Never at global scope for State | vcr.createSession() | Random, unreachable by other replicas |