Designing a Real-Time Chat Routing System for Customer Support
A production-grade walkthrough of matching incoming support conversations to the best available agent, in real time, at scale — the way it is built at companies like Zendesk, Intercom, Salesforce Service Cloud, and Amazon Connect. Queueing theory, fairness, exactly-once assignment, and graceful degradation, all under a sub-second latency budget.
Introduction & History
Every time you open a chat widget on a company’s website and type “I need help with my order,” something has to decide, within a second or two, which human being on the other side should answer you. That decision — invisible to the customer — is one of the more interesting real-time matching problems in software: it blends queueing theory, real-time systems, and a surprising amount of business logic (skills, priority tiers, language, SLAs) into a single split-second routing decision, repeated potentially thousands of times per minute across a large support organization.
This tutorial designs that system: a platform that accepts incoming chat conversations from customers and routes each one, in real time, to the most appropriate available support agent, at scale, without dropping conversations, and while respecting service-level agreements (SLAs) and fairness across the agent workforce.
1.1 A brief history
1990s — Call center ACD
The direct ancestor of chat routing. Phone-based Automatic Call Distributors routed inbound calls to available agents using simple round-robin or longest-idle-agent rules — the foundational ideas (queues, agent states, skill groups) all originate here.
Early 2000s — Live chat widgets emerge
Tools like LivePerson brought text-based chat to websites, initially with very simple routing — often just “first available agent” — because chat volumes were still low.
2007 – 2010 — Skill-based routing matures
As support organizations grew specialized teams (billing, technical, sales), routing engines started matching conversation attributes to agent skills rather than treating all agents as interchangeable.
2011 – 2015 — SaaS helpdesks
Zendesk, Intercom, and Freshdesk popularized SaaS helpdesk platforms. Chat became a first-class support channel alongside email and tickets, and routing logic became a configurable product feature rather than a bespoke call-center build.
2016 – 2019 — Omnichannel routing
Support organizations wanted a single queue and a single routing brain across chat, email, social media, and voice, rather than separate silos per channel — this is when routing engines evolved from “chat-specific” to genuinely channel-agnostic.
2018 – present — Bots and AI-assisted routing
Chatbots and AI deflect a portion of conversations entirely (self-service resolution) and, for the conversations that do reach a human, machine-learning models increasingly assist prioritization (predicting conversation urgency, likely resolution time, or churn risk) and agent-conversation fit.
2023 – present — LLM-based triage and co-pilot routing
Large language models now commonly perform initial intent classification and summarization before a conversation is routed, feeding richer signals (not just a customer-selected topic dropdown) into the routing decision.
“Why is chat routing harder than round-robin ticket assignment?” Chat is synchronous and real-time — a customer is actively waiting, agent availability changes second-to-second (not just per shift), and a bad match (wrong skill, over-capacity agent) is immediately visible to a live human rather than silently sitting in an email queue.
1.2 Functional and non-functional requirements
Functional requirements
- Accept incoming chat conversations from multiple channels (website widget, mobile app, social messaging) into a unified queue.
- Track real-time agent state: online / offline, available / busy, current active conversation count, skills, languages.
- Route each incoming conversation to the single best available agent based on skill match, workload, priority, and business rules.
- Support priority tiers (for example, enterprise customers, SLA-bound conversations) that can jump the queue.
- Support agent capacity limits (max concurrent chats per agent) and overflow or escalation when no agent is available.
- Allow supervisors to monitor queue depth, wait times, and manually reassign conversations.
- Preserve conversation context (prior messages, customer history) across any reassignment or transfer.
Non-functional requirements
- Latency: routing decisions should complete in well under a second from conversation creation to agent assignment under normal load, since customers are actively watching a “connecting you to an agent” indicator.
- Consistency: exactly one agent must be assigned per conversation — double-assignment (two agents both told they own the same chat) is a severe correctness bug, not just a UX annoyance.
- Fairness: workload should be distributed reasonably evenly across agents with equivalent skills, both for morale and for contractual or labor reasons in many support organizations.
- Availability: the routing system is on the critical path for every single incoming conversation — an outage means customers wait indefinitely or conversations silently vanish, so this needs very high uptime.
- Scalability: must handle large spikes in conversation volume (for example, an outage-driven support surge) without routing decisions degrading in latency or quality.
1.3 Why this problem resembles (and differs from) other matching systems
If you have studied ride-hailing dispatch (matching riders to drivers) or job-scheduling systems (matching tasks to workers), chat routing will feel familiar — it is the same underlying shape: a stream of “requests” and a pool of “resources,” matched continuously under changing conditions. But a few differences matter enormously for the design:
- The wait is visible and synchronous. A ride-hailing rider tolerates a few minutes of wait somewhat passively; a chat customer is staring at a live typing indicator or a “connecting…” message, and even a 10 to 15 second delay feels long. This pushes the latency budget for routing decisions much tighter than many other matching systems.
- Agents are stateful and finite over a shift, not per-trip. A driver finishes one ride and is immediately available for the next; a support agent typically juggles several concurrent conversations up to a configured capacity, so “availability” is a continuous, multi-valued quantity (how much headroom does this agent have right now) rather than a simple binary.
- Match quality has a long tail of business rules. Ride matching optimizes largely on proximity and ETA; chat routing optimizes on skill, language, priority tier, contractual SLA, and sometimes relationship continuity — a much richer and more configurable scoring surface.
- Conversations do not “complete” cleanly. A ride has an unambiguous end (drop-off); a support conversation might be transferred, escalated, reopened by the customer hours later, or merged with a related ticket — the lifecycle model has to accommodate much messier real-world flows.
1.4 Back-of-the-envelope capacity estimation
Sizing the problem before designing the solution, as you would in an interview: assume a large support organization handling on the order of tens of thousands of concurrent live chat conversations across its whole platform at peak, spread across many customer accounts and many agent teams. If a typical agent can competently manage 3 to 6 concurrent conversations, and peak concurrent conversations run into the tens of thousands, the agent workforce needed at peak is naturally in the low thousands — meaning the presence and capacity service needs to track state for thousands of agents with sub-second freshness, while the routing engine needs to process new-conversation and capacity-freed events fast enough to keep the queue-depth-to-resolution ratio stable rather than growing unbounded during sustained peak load. This estimation is what justifies partitioning the matching problem by skill or queue group early in the design, rather than treating it as one giant global matching computation.
Architecture & Components
At its core, this system is a real-time matching problem between two dynamic, fast-changing sets: incoming conversations (each with attributes like topic, priority, language, customer tier) and available agents (each with attributes like skills, current load, shift status). The architecture needs to keep both sides’ state fresh to the second and make a matching decision fast enough that the customer never notices a routing delay.
graph TB
subgraph CLIENTS["Customer Facing Clients"]
WEB["Web Chat Widget"]
MOB["Mobile App"]
SOC["Social Messaging Channels"]
end
subgraph INGRESS["Ingress Layer"]
LB["Load Balancer"]
GW["Conversation Gateway WebSocket"]
end
subgraph CORE["Routing Core"]
INTAKE["Conversation Intake Service"]
CLASSIFY["Intent Classification Service"]
QUEUE["Priority Queue Manager"]
ROUTER["Routing Engine"]
PRESENCE["Agent Presence Service"]
end
subgraph AGENTSIDE["Agent Facing"]
AGWEB["Agent Desktop Console"]
AGWS["Agent WebSocket Gateway"]
end
subgraph DATA["Data Layer"]
DB["Conversation Ticket DB"]
CACHE["Redis Agent State"]
STREAM["Event Stream Kafka"]
CRM["Customer Profile CRM"]
end
WEB --> LB
MOB --> LB
SOC --> LB
LB --> GW
GW --> INTAKE
INTAKE --> CLASSIFY
CLASSIFY --> QUEUE
QUEUE --> ROUTER
ROUTER --> PRESENCE
PRESENCE --> CACHE
ROUTER --> AGWS
AGWS --> AGWEB
AGWEB --> PRESENCE
INTAKE --> DB
CLASSIFY --> CRM
ROUTER --> STREAM
STREAM --> DB
2.1 Core components
Conversation Gateway
Terminates customer-facing WebSocket or long-poll connections; normalizes messages from different channels into a common internal event format.
Conversation Intake Service
Creates a conversation record, attaches customer identity and context, and hands it off for classification and queueing.
Intent Classification Service
Analyzes the initial message (and customer metadata) to infer topic, urgency, and required skill — using rules, ML models, or increasingly an LLM-based classifier.
Priority Queue Manager
Holds waiting conversations ordered by priority and wait time, partitioned by skill or queue group.
Routing Engine
The matching brain — continuously (or event-driven) pairs waiting conversations with available agents based on configured strategy.
Agent Presence Service
Tracks real-time agent state: online, away, or offline; current active conversation count; skills; and capacity limits.
Agent Desktop / Console
The UI agents use to receive assigned conversations, see customer context, and update their own status or capacity.
Event Stream
Durable log of all conversation lifecycle events (created, queued, assigned, transferred, closed) — feeds analytics, SLA tracking, and downstream systems.
Zendesk’s “Omnichannel Routing” maintains a real-time capacity model per agent (a configurable max number of concurrent conversations, often weighted differently per channel — for example, a chat “costs” more capacity than an email) and continuously assigns from a prioritized queue as capacity frees up, rather than only routing at the moment a new conversation arrives.
2.2 Component deep dive: the Intent Classification Service
Classification quality directly determines routing quality — if the system mis-tags a billing question as “general inquiry,” the customer lands in the wrong skill queue and the eventual agent has to redirect them, wasting everyone’s time. Modern implementations layer several signals:
- Explicit customer input: a topic dropdown, a pre-chat form, or a bot’s initial “what can I help you with” flow — the most reliable signal when available, but not always present (many customers skip optional forms).
- Lightweight rules or keyword matching: fast, deterministic, cheap — good for high-confidence, clearly-worded requests (“cancel my subscription” almost always means billing or account).
- ML classifier or LLM-based intent extraction: handles ambiguous or multi-topic messages better than rules, at the cost of added latency and compute — often used as a fallback when rules produce low confidence, rather than for every single message.
- Customer / account metadata: account tier, product line, past conversation history — used to bias classification and set priority even before the message content is fully analyzed.
A key architectural decision is that classification must have a bounded worst-case latency with a safe default: if the classifier is slow, times out, or is simply unavailable, the conversation should fall into a general or default queue rather than block indefinitely waiting for a perfect classification.
2.3 Component deep dive: the Agent Presence Service
This service is deceptively small in scope but sits directly in the hottest path of the entire system — nearly every routing decision reads from it, and every agent status change writes to it. It typically maintains, per agent: online / away / offline status, current active conversation count, configured max capacity (often per-channel-weighted), skill and language tags, and a last-heartbeat timestamp used to detect silent disconnects. Because this data is read far more often than it is written (many routing evaluations per status change), it benefits from being served out of an in-memory store with read replicas, and from being denormalized or pre-filtered by skill group so the routing engine does not need to scan the entire agent population for every match attempt.
Internal Working — The Routing Engine
The routing engine is the component worth understanding most deeply, because “route to the most appropriate available agent” is deceptively simple to state and genuinely hard to implement correctly at scale.
3.1 The core matching loop
Conceptually, the routing engine runs (or reacts to) a matching loop with two possible triggers:
- Event-driven trigger: a new conversation arrives, or an agent becomes available (finishes a chat, comes online) — either event should immediately attempt a match rather than waiting for a fixed polling interval, since polling introduces unnecessary latency into a customer-facing wait time.
- Periodic reconciliation: a lower-frequency background pass (for example, every few seconds) re-evaluates the queue to catch any conversations that should have been matched but were not — for instance, due to a race condition or a transient failure in the event-driven path. This acts as a safety net, not the primary mechanism.
3.2 Avoiding double-assignment: the concurrency problem
The single most important correctness property in this system is that exactly one agent gets assigned to a given conversation, even when multiple routing attempts might be evaluating the same agent’s availability simultaneously (for example, two conversations both eligible for the same lone available specialist). This is a classic distributed concurrency problem, and it is solved the same way most “exactly one winner” problems are solved:
- Atomic compare-and-swap on agent capacity: when the router decides to assign a conversation to an agent, it performs an atomic decrement of that agent’s remaining capacity (for example, a Redis DECR with a floor check, or a conditional update) — if the decrement would take capacity below zero, the assignment is rejected and the router retries with the next-best agent.
- Single-writer partitioning: an alternative approach partitions agents (and their queues) across router instances by consistent hashing, so only one router instance ever makes assignment decisions for a given agent, eliminating the race entirely rather than resolving it after the fact.
“How do you prevent two conversations from both being assigned to the same agent’s last open slot?” This is one of the most common follow-up questions for this design. The strongest answers describe an atomic, single-source-of-truth capacity check (not “read capacity, then separately write assignment,” which is a classic check-then-act race condition) — either via an atomic data-store operation or by architecturally guaranteeing only one process ever decides for a given agent.
3.3 Scoring candidate agents
For a given waiting conversation, the router typically narrows the agent pool to those matching hard requirements (skill, language, currently online, has capacity) and then scores the remaining candidates to pick the best one:
| Scoring Factor | Why It Matters |
|---|---|
| Skill match quality | An agent whose primary skill exactly matches the conversation’s classified topic is preferred over one with only a secondary or tangential skill. |
| Current load relative to capacity | Prefer agents further from their max capacity, to spread load evenly and avoid overloading any one agent. |
| Idle time / longest-available | Among otherwise-equal candidates, prefer the agent who has been idle longest — this is the direct descendant of call-center “longest idle agent” routing and promotes fairness. |
| Historical performance / fit | Some systems factor in an agent’s historical resolution time or satisfaction score for similar conversation types — a data-driven refinement layered on top of the base rules. |
| Customer relationship continuity | If the customer has an existing relationship with a specific agent (for example, an assigned account manager), that agent may be preferred even outside strict availability rules, subject to business configuration. |
3.4 Handling multi-skill agents and overlapping queues
Most real support agents have more than one skill (for example, billing and general account questions), which means the clean picture of “one skill group, one isolated queue” breaks down in practice — an agent might be eligible for several queues simultaneously, and the routing engine needs a policy for which queue’s conversation they receive when both have waiting work. A common approach assigns each agent a primary and secondary skill ranking, and the router prefers matching them against their primary-skill queue first, only pulling from secondary queues when the primary queue is empty or the agent has been idle beyond a threshold — this keeps specialist capacity available for the work it is best suited for while still preventing agents from sitting idle when cross-trained work exists.
3.5 Conversation batching under extreme load
Under normal load, event-driven one-at-a-time matching (a conversation arrives, immediately find its best agent) is optimal for latency. But under extreme burst conditions — many conversations and many available agents changing state within the same few hundred milliseconds — naive one-at-a-time matching can produce suboptimal global assignments (for example, greedily assigning an okay-fit agent to conversation A when a slightly-later-arriving conversation B would have been a much better fit for that same agent). Some systems address this with brief micro-batching: collect events within a small window (on the order of 100 to 300 ms), then run a bipartite matching optimization (for example, a variant of the Hungarian algorithm or a simpler greedy-by-best-score approach) across the whole batch at once. This trades a small, bounded amount of added latency for meaningfully better aggregate match quality during bursts — a classic example of relaxing a per-item optimization in favor of a batch-level one.
Data Flow & Lifecycle
Walking through a single conversation end to end — from first message to agent assignment — makes the coordination between intake, classification, queue, presence, and router concrete.
4.1 Conversation lifecycle
sequenceDiagram
participant Cust as Customer
participant GW as Conversation Gateway
participant Intake as Intake Service
participant Classify as Classification Service
participant Q as Queue Manager
participant Router as Routing Engine
participant Presence as Agent Presence Service
participant Agent as Agent Desktop
Cust->>GW: Opens chat and sends first message
GW->>Intake: Create conversation record
Intake->>Classify: Classify intent urgency skill
Classify->>Intake: topic billing priority standard
Intake->>Q: Enqueue conversation skill billing
Q->>Router: Notify new conversation queued
Router->>Presence: Query available billing skilled agents
Presence->>Router: Agent A idle 40 seconds Agent B idle 5 seconds
Router->>Presence: Atomic capacity decrement Agent A
Presence->>Router: Success
Router->>Agent: Assign conversation to Agent A
Router->>Cust: Notify connected to agent
Agent->>Cust: Hi how can I help
4.2 Agent status change lifecycle
Agent presence is just as real-time as conversation intake. When an agent finishes a chat, goes on break, or logs in for their shift, the presence service must reflect that change immediately, because a stale “available” status leads to a conversation being routed to someone who is not actually free (a serious failure mode), while a stale “busy” status wastes available capacity and inflates customer wait times unnecessarily.
- Agent action (closes chat, clicks “away,” logs out) is sent to the presence service via the agent WebSocket connection.
- Presence service updates the agent’s state in the shared cache (Redis) with a short TTL-based heartbeat, so a crashed or disconnected agent client is automatically marked unavailable after a brief timeout rather than staying “available” forever.
- A capacity-freed event triggers the router to immediately attempt to fill that newly available slot from the queue, rather than waiting for the next conversation-side event.
4.3 Transfer and escalation lifecycle
Conversations do not always end with the first-assigned agent — they get transferred to a specialist, escalated to a supervisor, or handed off at shift change. Each of these is modeled as a re-entry into the routing engine with the existing conversation’s full context (message history, customer profile, prior classification) attached, rather than as a brand-new conversation — this preserves continuity for the customer, who should not have to repeat themselves, and gives the newly assigned agent everything the router and previous agent already knew.
4.4 Handling reopened conversations
Customers frequently return to a conversation hours or days after it appeared “closed” — replying to a resolved chat, or a bot escalating a previously-deflected inquiry. The lifecycle model needs to distinguish a genuinely new conversation from a reopened one, because a reopened conversation typically carries strong affinity signal toward the previously-assigned agent (continuity matters for customer experience) and should not necessarily re-enter the general queue from scratch. A common pattern is a configurable reopen window (for example, 24 to 72 hours) during which a new customer message on a closed conversation is routed back to the original agent if they are still available, and falls back to standard routing only if that agent has left the team, is offline for an extended period, or the reopen window has expired.
Routing Algorithms & Matching Strategies
This is the heart of the system design — the actual algorithmic decision of who gets what.
5.1 Common routing strategies
| Strategy | How It Works | Best For |
|---|---|---|
| Round Robin | Cycles through available agents in a fixed order regardless of skill or load nuance. | Simple, homogeneous teams where every agent can handle any conversation equally well. |
| Longest Idle Agent | Always routes to whichever eligible agent has been available or idle the longest. | Fairness-focused teams; naturally self-balances load over time without explicit load tracking. |
| Skill-Based Routing | Filters agents by required skill(s) or language before applying a secondary tiebreaker (for example, longest idle). | Specialized support organizations with distinct topic areas (billing, technical, sales). |
| Load-Based / Capacity-Weighted | Scores agents by how much headroom they have relative to their configured max capacity, preferring the least-loaded eligible agent. | Teams where agents can handle multiple concurrent chats and capacity varies per agent (for example, senior agents handle more). |
| Priority Queueing | Maintains separate priority tiers (for example, enterprise SLA customers) that are served ahead of standard-tier conversations within the same skill group. | Organizations with tiered support contracts or SLA commitments. |
| Affinity / Relationship | Prefers an agent who has previously interacted with this specific customer, when available. | High-touch account-based support models. |
| Predictive / ML-Assisted | Uses a trained model to predict which agent is statistically likely to resolve this specific conversation fastest or most successfully, based on historical patterns. | Large-scale operations with enough historical data to train reliable models; typically layered on top of, not instead of, the rule-based strategies above. |
“Would you use pure round robin or something smarter?” Pure round robin is rarely sufficient in production because it ignores skill fit and current load entirely — most real systems combine a hard skill or eligibility filter with a load-aware or longest-idle tiebreaker, and layer priority tiers on top for SLA-bound customers.
5.2 Priority queue design
The queue is not a single FIFO list — it needs to support multiple priority tiers while still preventing starvation of lower-priority conversations. A common approach is a weighted / aging priority queue: each conversation has a base priority (from its tier or SLA), but its effective priority increases the longer it waits, ensuring that even a standard-tier conversation eventually rises above a newly-arrived high-priority one if it has been waiting long enough — this prevents an organization’s lowest-priority customers from waiting indefinitely during sustained high-priority volume.
graph LR
A["New Conversation"] --> B{"Priority Tier"}
B -->|"Enterprise SLA"| C["High Priority Queue"]
B -->|"Standard"| D["Standard Priority Queue"]
B -->|"Low or Best Effort"| E["Low Priority Queue"]
C --> F["Aging Function"]
D --> F
E --> F
F --> G["Effective Priority Score"]
G --> H["Router selects highest scoring eligible conversation per available agent"]
5.3 Handling no available agent — overflow strategies
Real-time routing must define explicit behavior for the case where no eligible agent is currently available — this is not an edge case, it is a routine operating condition during peak load:
Queue with ETA
The customer sees an honest queue position and estimated wait, computed from current queue depth and historical resolution rates.
Broader Skill Group
After a configurable wait threshold, relax the skill requirement (for example, allow any agent, not just billing specialists) rather than let the customer wait indefinitely for a narrow specialist pool.
Secondary Team / Region
Route to a different regional or outsourced support team when the primary team is saturated.
Bot / Self-Service
Offer an AI-assisted or self-service resolution path while waiting, which can fully resolve a meaningful fraction of conversations without ever reaching a human agent.
Callback / Async
Convert the real-time chat into an asynchronous ticket with a promised response time, when live wait times exceed an acceptable threshold.
5.4 Fairness vs. optimality
There is a genuine tension between routing every conversation to the objectively “best” agent (by skill or performance score) and distributing workload fairly across the team. Always routing to the single top-scoring agent creates burnout risk and morale issues for your best performers while under-utilizing everyone else. Most production systems address this with a bounded randomization or weighted-lottery approach among the top-N eligible candidates, rather than a strict deterministic “always pick the single best match,” trading a small amount of theoretical optimality for sustainable workload distribution.
5.5 Worked example: scoring function
It helps to make the scoring step concrete. A typical composite score for a candidate agent, given a specific waiting conversation, might combine several normalized (0 to 1) sub-scores with configurable weights:
- Skill match score (weight ~0.4): 1.0 for an exact primary-skill match, lower for secondary or tangential skill matches.
- Capacity headroom score (weight ~0.3): proportion of unused capacity remaining, so agents further from their max load score higher.
- Idle time score (weight ~0.2): normalized time since the agent’s last assignment, rewarding longer-idle agents.
- Relationship / continuity score (weight ~0.1): a boost if this agent has prior history with the specific customer.
The router computes this composite score for every eligible candidate, then either picks the single highest-scoring agent or samples from among the top few candidates with probability proportional to score (the bounded-randomization approach discussed above). Making these weights configurable per team or queue, rather than hardcoded, is important in practice — a sales queue might weight relationship continuity much more heavily than a general billing queue would.
5.6 Bot deflection as part of the routing funnel
It is worth explicitly modeling bot or self-service resolution as the first stage of the overall routing funnel, not a separate system. A well-designed platform attempts automated resolution (FAQ matching, guided self-service flows, or an LLM-based assistant) before a conversation ever reaches the human-agent queue, and only escalates to human routing when the automated attempt fails, the customer explicitly requests a human, or the classified intent or priority indicates automated handling is inappropriate (for example, an angry or high-value customer). This meaningfully changes the capacity-planning math for the human-agent routing system, since a well-tuned bot layer can deflect a substantial fraction of total volume, leaving human routing to handle the harder, lower-volume remainder.
Advantages, Disadvantages & Trade-offs
Every meaningful design choice in this system is a bet on which trade-off is worth accepting. The table below captures the biggest ones.
| Decision | Advantage | Disadvantage / Trade-off |
|---|---|---|
| Event-driven matching over polling | Minimal routing latency; agents and customers both see near-instant assignment. | More complex to implement correctly (race conditions, exactly-once assignment) than a simple periodic batch job. |
| Skill-based routing over round robin | Better conversation-agent fit, higher first-contact resolution rates. | Requires accurate skill tagging or classification, and risks narrow specialist pools becoming bottlenecks. |
| Aging priority queue over strict priority | Prevents starvation of lower-priority conversations. | Adds complexity to queue scoring and requires careful tuning of aging rates. |
| Bounded randomization over strict best-match | Sustainable, fair workload distribution across the team. | Slightly lower average conversation-agent fit compared to always picking the theoretically optimal agent. |
| Single-writer partitioning for agent assignment | Eliminates double-assignment races architecturally. | Requires careful partition rebalancing when router instances scale up, down, or fail. |
6.1 Trade-off: real-time matching vs. batch optimization
Pure event-driven, one-at-a-time matching minimizes per-conversation latency but can produce globally suboptimal assignments during bursts, as discussed earlier. Pure batch matching maximizes global assignment quality but adds latency to every single conversation, even during quiet periods when there is no real contention to optimize around. The right answer in production is almost always adaptive: default to immediate one-at-a-time matching, and only shift into short batching windows when queue depth or event rate crosses a threshold that suggests contention is actually happening — applying the more expensive optimization exactly when it earns its cost, and not before.
6.2 Trade-off: centralized vs. partitioned matching state
A single global view of all agents and all queued conversations would, in principle, allow the theoretically best possible matches across the entire organization at every instant. In practice, this centralization becomes a scaling and availability bottleneck — every routing decision would contend on the same global state. Partitioning by skill or queue group sacrifices a small amount of theoretical optimality (an agent in queue A can never be matched to a conversation in queue B, even if they would technically be a decent fit) in exchange for the ability to scale matching horizontally and isolate failures to a single partition rather than the whole platform.
6.3 Trade-off: push assignment vs. pull / self-selection
As touched on above, an assignment-based (“push”) model gives the platform strong control over fairness and SLA compliance but removes agent autonomy over which conversations they take. A self-selection (“pull”) model preserves agent autonomy and can improve match quality when agents have good self-awareness of their own strengths, but weakens the platform’s ability to guarantee wait-time fairness and makes cherry-picking of easy conversations a real operational risk. Many mature platforms land on a middle ground: the system pushes assignments by default to guarantee baseline SLA behavior, while allowing agents limited visibility and reordering within their own already-assigned queue.
Performance & Scalability
This system does not have to serve a massive request rate compared to, say, an ad exchange — but every single request is on the critical path of a live human waiting for a response, and that changes what “fast enough” means.
7.1 Latency budget
From the moment a conversation is queued to the moment an agent is assigned, the target is typically well under a second under normal load — this budget breaks down roughly as: classification (10 to 100 ms, more if using an LLM-based classifier, less for a lightweight rules or ML model), queue insertion and eligibility filtering (a few milliseconds against an in-memory index), scoring candidates (a few milliseconds for a bounded candidate set), and the atomic capacity-assignment operation (single-digit milliseconds against a fast key-value store like Redis).
7.2 Scaling the matching engine
- Partition by queue or skill group: since conversations are only ever matched against agents within their eligible skill group, the entire matching problem can be sharded by skill or queue, allowing independent router instances to handle different queues in parallel without contention.
- In-memory agent state: agent presence and capacity are read (and often written) extremely frequently during matching — this state belongs in a fast in-memory store (Redis or similar) rather than a relational database, which would become a bottleneck under high-frequency read / write matching traffic.
- Batch reconciliation under load spikes: during extreme volume spikes, briefly shifting from pure per-event matching to small time-windowed batch matching (for example, every 200 ms, match all queued conversations against all available agents at once using a bipartite-matching-style approach) can improve overall throughput and match quality at the cost of a small, bounded added latency.
“How would this system handle a 10x spike in incoming chats during a major outage?” Queue depth grows and wait times increase gracefully rather than the system failing outright — admission control still accepts conversations into the queue, overflow, skill-relaxation, and bot deflection kick in automatically past configured thresholds, and router instances scale horizontally per skill-partition since the matching workload is naturally shardable.
7.3 Queueing theory lens
It is genuinely useful to think about this system through the lens of classic queueing theory (an M/M/c-style multi-server queue): conversations arrive following some arrival process, agents are the “servers,” and each conversation occupies a server for a variable “service time” (the conversation’s duration). Two practical implications follow directly from this framing:
- Utilization near 100 percent causes wait times to explode non-linearly. Queueing theory shows that as server utilization approaches full capacity, average wait time grows much faster than linearly — this is why staffing and capacity planning targets meaningful headroom (agents rarely 100 percent utilized) rather than trying to keep every agent maximally busy at all times, even though that looks inefficient on a simple utilization dashboard.
- Variance in conversation length matters as much as the average. A queue with highly variable conversation durations (some resolved in 1 minute, others taking 45) produces worse average wait times than a queue with the same average duration but low variance — this is why some systems separate “quick win” conversation types (password resets, simple status checks) into their own fast-turnaround queue rather than mixing them with long, complex conversations.
High Availability & Reliability
The routing platform is on the critical path for every incoming conversation, so its availability requirements are stricter than most SaaS services — an outage does not just delay work, it leaves live customers staring at an unresponsive screen.
- No single point of failure in the matching path: router instances should be horizontally replicated per partition, with automatic failover if an instance holding a given skill-partition dies — another instance picks up that partition’s queue and agent set.
- Durable queue, ephemeral matching: the queue of waiting conversations must be durably persisted (not just held in a single process’s memory) so a router crash never silently drops a customer who has been waiting — a replicated data store or a durable log (Kafka) backs the queue state.
- Graceful degradation on classification failure: if the intent classification service is slow or unavailable, the system should fall back to a default or general skill group rather than blocking the entire conversation from being queued.
- Presence heartbeat timeouts: an agent’s “available” status must automatically expire if their client stops heartbeating, preventing conversations from being routed to an agent who has silently disconnected.
- Idempotent assignment handling: if an assignment message to an agent’s client is retried due to a network blip, the agent desktop must handle duplicate delivery safely rather than showing the same conversation twice.
Intercom’s routing infrastructure separates the durable conversation and queue state from the real-time matching computation specifically so that a transient issue in the matching layer does not risk losing track of a customer who is actively waiting — the queue itself remains the durable source of truth.
8.1 Failure mode analysis
| Failure | Impact if Unhandled | Mitigation |
|---|---|---|
| Router instance crash mid-partition | All conversations and agents in that skill partition stop being matched. | Partition ownership is leased with a short TTL; another instance detects the lapsed lease and takes over. |
| Presence store outage | Router cannot determine agent availability, halting all matching. | Presence store is replicated with automatic failover; router falls back to a brief “hold and retry” rather than mis-routing against stale data. |
| Classification service outage | New conversations cannot be tagged with skill or priority. | Fallback to a default or general queue with lower priority rather than blocking intake entirely. |
| Queue store outage | Newly arriving conversations could be lost entirely. | Intake writes to a durable, replicated queue store synchronously before acknowledging the customer’s request as received. |
8.2 Regional failover for follow-the-sun support
Global support organizations often run agent pools that shift by time zone (a “follow-the-sun” model), and the routing system’s region-awareness needs to handle a full region going dark — whether due to an infrastructure outage or simply the end of that region’s staffed shift — without losing conversations that were mid-flight. The standard approach treats “region has no available agents” as just another input into the existing overflow chain (skill relaxation, cross-region routing) described earlier, rather than requiring a special-cased failure path — reusing the same graceful-degradation machinery for both a genuine outage and an entirely expected end-of-shift transition.
Security
Customer support conversations often contain PII, account details, and sensitive complaints, so the routing platform has to treat security as a first-class concern even though it is not itself a payments or authentication system.
- Customer-agent session authentication: both the customer’s chat session and the agent’s console session are authenticated and scoped — a customer’s conversation token should only grant access to their own conversation, and an agent’s session should only expose conversations actually assigned to them.
- PII handling in classification: intent classification (especially LLM-based) often processes potentially sensitive customer text — this data needs appropriate encryption in transit and at rest and adherence to data retention and regional data-residency policies (for example, GDPR).
- Audit trail for assignment decisions: every routing decision (which agent, why, at what priority) should be logged immutably, both for debugging and for compliance and dispute resolution (for example, “why did this VIP customer wait 20 minutes?”).
- Rate limiting and abuse protection: the intake endpoint needs rate limiting per customer or IP to prevent a flood of fake conversations from exhausting agent capacity or overwhelming the queue.
- Role-based access for supervisors: manual reassignment and queue-monitoring tools must be restricted to authorized supervisor roles, since they can directly affect customer wait times and agent workload.
9.1 Threat model specific to routing systems
Queue-Flooding DoS
A malicious actor spamming fake conversations could exhaust agent capacity or dramatically inflate legitimate customers’ wait times — intake rate limiting and anomaly detection on conversation-creation patterns (for example, many conversations from the same IP or device in a short window) are necessary defenses.
Agent Impersonation
An attacker gaining access to an agent’s console session could view other customers’ sensitive conversation history — strict session scoping (an agent’s client only ever receives data for conversations actually assigned to them, enforced server-side, not just hidden in the UI) prevents this.
Priority Manipulation
If priority tier is derived from client-supplied data without server-side verification against the actual customer or account record, a malicious customer could attempt to falsely claim a high-priority tier to jump the queue — priority must always be derived from authoritative server-side account data, never trusted from client input.
9.2 Data minimization in classification logging
Since intent classification logs typically capture snippets of customer message content for debugging and model improvement, it is worth deliberately minimizing what gets retained long-term — storing derived labels (topic, urgency, confidence score) durably while treating raw message text as short-retention data purged on a defined schedule, unless the customer has consented to longer retention for support-quality purposes. This reduces the blast radius of a potential data exposure and keeps the system aligned with data-minimization principles common to privacy regulations.
Monitoring, Logging & Metrics
A routing platform’s health cannot be inferred from CPU dashboards alone — the meaningful signals are customer-facing (wait time, abandonment) and operational (fairness, matching latency).
| Metric Category | Examples |
|---|---|
| Customer Experience | Average and percentile wait time, queue abandonment rate, time-to-first-response, SLA compliance rate. |
| Routing Quality | Skill-match rate, first-contact resolution rate by routing path, transfer or reassignment rate. |
| Agent Workload | Concurrent conversations per agent vs. capacity, idle time distribution, workload variance across the team (a fairness signal). |
| System Health | Matching decision latency, queue depth over time, router instance error or retry rate, presence heartbeat failure rate. |
Wait time and abandonment rate deserve special attention as leading indicators: a rising abandonment rate (customers giving up before being connected) often signals a routing or capacity problem well before it shows up in aggregate SLA compliance numbers, since it is measuring the customers who never got far enough to be counted in a “resolved within SLA” statistic.
10.1 Dashboards for different audiences
The same underlying event stream needs to power meaningfully different dashboards for different roles: supervisors need a live queue view (current wait times, agents by status, any conversation approaching SLA breach) to intervene in real time; workforce planning teams need aggregated historical trends (volume by hour or day, staffing-to-demand ratios) to plan future shifts; and engineering needs system-health dashboards (matching latency percentiles, partition rebalance events, error rates) to keep the platform itself healthy. Designing the event schema generically enough to serve all three from one durable log, rather than building bespoke pipelines for each, keeps the system maintainable as new reporting needs inevitably emerge.
Deployment & Cloud
The routing layer’s statelessness (durable state lives elsewhere) makes deployment mostly conventional, but a few workload-specific choices matter.
- Stateless router instances behind partition-aware routing: router processes themselves can be deployed as standard containerized, horizontally-scalable services, since durable state lives in the shared cache or queue layer — this makes rolling deploys and autoscaling straightforward compared to a stateful media system.
- Multi-region deployment for global support operations: large organizations often run regional agent pools (follow-the-sun support); the intake and routing layer should be deployable per region while still supporting cross-region overflow when a region’s local agent pool is saturated.
- Blue-green deploys are low-risk for this layer: because router instances are stateless and the queue and presence state lives externally, new versions can be rolled out with standard blue-green or canary strategies without needing to drain long-lived sessions the way a stateful media server would.
- Autoscaling driven by queue depth, not just CPU: because the workload is bursty (support volume spikes are common and event-driven, for example, a product outage), autoscaling policies should react to queue depth and wait-time metrics, not purely CPU or memory utilization, to respond fast enough to genuine demand spikes.
Databases, Caching & Load Balancing
No single data store fits every workload here — the system pairs a durable, queryable store for records with a fast in-memory store for hot state, and a durable log for lifecycle events.
| Layer | Technology Pattern | Why |
|---|---|---|
| Conversation / ticket records | Relational or document DB with read replicas. | Needs durability and query flexibility for reporting, history, and compliance. |
| Agent presence & capacity state | In-memory store (Redis) with atomic operations and TTL-based heartbeats. | Extremely high read and write frequency during matching; must support atomic compare-and-swap-style updates. |
| Priority queue | Redis sorted sets (score = effective priority) or a dedicated queueing system, partitioned by skill group. | Needs fast insertion, fast “get highest priority eligible item,” and durability against process crashes. |
| Event / analytics pipeline | Kafka or similar durable log. | Decouples real-time routing decisions from downstream analytics, SLA reporting, and ML model training pipelines. |
| Customer profile / CRM data | Dedicated CRM service or data store, queried (and often cached) during classification and routing. | Enables relationship-based routing and richer classification without embedding all customer data directly in the hot path. |
| Load balancing (ingress) | Layer-7 load balancer with sticky WebSocket sessions per customer or agent connection. | Chat connections are long-lived and stateful at the connection level, similar to any real-time messaging system. |
12.1 Why presence state and conversation records are deliberately split
As with any real-time matching system, it is tempting to store everything about an agent or conversation in one place, but presence and capacity state and durable conversation records have fundamentally different consistency and durability needs. A conversation’s final transcript and resolution must be durably preserved for compliance, reporting, and dispute resolution — losing it is unacceptable. An agent’s momentary “currently handling 2 of 4 max conversations” count, by contrast, is fully reconstructable at any moment by simply counting their currently-assigned open conversations — losing it in a cache restart is an inconvenience, not a data-loss event. Recognizing which state is truly durable and which is a derived, reconstructable cache is what justifies putting the two in very differently-optimized stores.
APIs & Microservices
The natural service boundaries mirror the distinct responsibilities and scaling profiles in this system.
Conversation Gateway
Connection-heavy, mostly stateless, horizontally scaled behind sticky load balancing.
Intake Service
Moderate throughput, creates durable records, relatively simple CRUD-style API.
Classification Service
Potentially the most compute-intensive component if using ML or LLM-based classification; benefits from independent scaling and possibly GPU-backed infrastructure, decoupled from the lighter-weight routing logic.
Queue Manager
High-frequency read and write against the priority queue store, partitioned by skill group for horizontal scale.
Routing Engine
The matching brain; partitioned by skill group, needs low-latency access to both queue and presence state.
Agent Presence Service
Very high read and write frequency, in-memory backed, needs to be fast above almost all other concerns.
“Would you combine the Queue Manager and Routing Engine into one service?” Reasonable either way. Combining them reduces network hops in the hot path (lower latency), while separating them allows each to scale and evolve independently — many production systems start combined and split them only once the queue-management and matching-logic workloads genuinely diverge in scaling needs.
Design Patterns & Anti-Patterns
The patterns below keep reappearing in production routing platforms; the anti-patterns are the ones that keep sinking the ones that skipped them.
14.1 Patterns to use
Atomic CAS for Assignment
Guarantees exactly-one-agent-per-conversation correctness under concurrent matching attempts.
Partition by Skill / Queue
Makes the matching problem embarrassingly parallel and horizontally scalable.
Aging Priority Queues
Balance urgency-based prioritization with fairness and starvation prevention.
Graceful Overflow Chains
Skill relaxation, cross-region overflow, and bot deflection as successive fallback layers rather than a hard queue cap.
Event Sourcing for Lifecycle
An immutable log of every state transition (queued, assigned, transferred, closed) supports both reliability (replay and recovery) and analytics.
14.2 Anti-patterns to avoid
- Read-then-write assignment without atomicity. Checking an agent’s availability and then separately writing the assignment invites double-assignment races under concurrent load.
- Rely on polling-only matching. A fixed-interval batch job as the sole matching mechanism introduces unnecessary latency that customers directly experience as wait time.
- Always route to the single “best” agent. Ignoring fairness leads to burnout for top performers and idle time for others — a real operational and morale problem, not just a technical inefficiency.
- Treat agent presence as eventually-consistent-is-fine. Stale presence data routes conversations to unavailable agents, directly harming customer experience — this state needs to be as close to real-time as the matching decision itself.
- Skip starvation protection in priority queues. A strict priority queue without aging can leave low-priority conversations waiting indefinitely during sustained high-priority volume.
Best Practices & Common Mistakes
The gap between a demo-quality routing prototype and a production one lives in the practices below — and the mistakes on the other side of the ledger.
| Best Practice | Common Mistake It Avoids |
|---|---|
| Design the matching decision as atomic from the start. | Retrofitting concurrency safety after discovering double-assignment bugs in production. |
| Treat overflow and degradation paths as first-class requirements, not afterthoughts. | A system that works perfectly under normal load but has no defined behavior during a volume spike, leading to unbounded wait times or dropped conversations. |
| Log every routing decision with its contributing factors. | Being unable to explain to a customer or auditor why a specific routing decision was made. |
| Build fairness and workload-balance metrics in from day one. | Discovering months later that top performers are burning out from consistently receiving disproportionate load. |
| Keep classification and routing loosely coupled. | A slow or failing classification model (especially LLM-based) blocking the entire routing pipeline instead of degrading gracefully to a default queue. |
15.1 Testing routing logic in production safely
Because routing decisions directly affect real customer wait times and real agent workloads, testing changes to the scoring function or overflow thresholds carries genuine risk — a bad weight change could silently overload part of the team or spike wait times for a customer segment. The safest pattern is shadow-mode evaluation: run a candidate routing strategy alongside the live one, logging what it would have decided without actually acting on it, and compare the two strategies’ predicted outcomes (wait time, fairness, skill-match rate) over real traffic before ever promoting the new strategy to production, and even then rolling it out to a small percentage of traffic first rather than switching over all at once.
Real-World Industry Examples
The design decisions above are not academic — every major helpdesk and contact center platform runs some version of them, adapted to its scale and customer base.
Omnichannel Routing
Tracks agent capacity as a configurable numeric value per channel (chat, messaging, email, calls can each “cost” different amounts of an agent’s total capacity) and continuously assigns from prioritized queues as capacity frees up in real time.
Rule + Balanced Routing
Distinguishes between rule-based assignment (explicit team or skill routing rules configured by admins) and balanced or round-robin assignment within a team, giving support organizations a spectrum between fully deterministic and load-balanced routing strategies.
Service Cloud Omni-Channel
Supports both simple queue-based routing and a more sophisticated skills-based “Omni-Channel Flow” that can incorporate external scoring (including Einstein AI-based recommendations) into the agent-selection decision — an early production example of ML-assisted routing at scale.
Conversational Cloud
As one of the earliest live-chat platforms, LivePerson’s routing evolved from simple availability-based assignment toward “Conversational Cloud” AI-assisted routing that incorporates predicted conversation complexity and intent, informed by decades of accumulated chat interaction data.
Amazon Connect
Exposes routing as explicit, user-configurable “routing profiles” and priority-based queues, and integrates directly with Amazon Lex bots for pre-routing self-service deflection and with Contact Lens for AI-driven sentiment or intent analysis feeding into the routing decision — a clear production example of the classification-then-routing pipeline described in this tutorial.
Frequently Asked Questions
The questions below come up in nearly every real interview and design review of this system.
The routing engine must resolve this via an atomic assignment operation (for example, an atomic capacity decrement in Redis) so exactly one conversation wins the assignment; the other conversation remains queued and is immediately re-evaluated against the next-best eligible agent or stays queued if none exists.
Typically a hybrid: the initial queue insertion and intake are handled synchronously (the customer needs immediate confirmation their conversation was received), while the actual matching computation is event-driven and can run asynchronously — as long as it completes fast enough that the customer does not perceive added delay, which in practice means well under a second.
The presence service detects the disconnect (via heartbeat timeout), and the conversation is flagged for reassignment or supervisor intervention rather than left orphaned — many systems attempt automatic reassignment to another available agent with equivalent skill, preserving full conversation context so the customer does not need to repeat themselves.
Not universally — for small, generalist teams where every agent can competently handle any conversation type, the added complexity of skill-based routing may not pay for itself, and a simpler load-balanced or longest-idle strategy performs just as well with far less configuration overhead. Skill-based routing earns its complexity primarily in larger, more specialized support organizations.
The core matching engine, presence and capacity model, and priority queue design generalize well — the main changes are channel-specific intake adapters (normalizing each channel’s format into the common conversation event schema) and a capacity model that weights different channel types differently, since a synchronous chat typically “costs” more of an agent’s attention than an asynchronous email does. The routing and scoring logic itself stays largely channel-agnostic.
Self-selection (a “pull” model) is used by some organizations and has real advantages — agents can pick conversations matching their confidence level, which can improve resolution quality. But it introduces its own problems: cherry-picking (easy conversations get picked quickly, hard ones languish), inconsistent wait-time fairness across customers, and weaker SLA guarantees, since nothing forces prompt pickup of a high-priority item. Most large-scale systems use assignment (“push”) as the default specifically because it gives the platform direct control over SLA compliance and fairness, sometimes offering a limited self-selection option only within an agent’s own already-assigned queue.
Summary & Key Takeaways
Every design decision in this tutorial follows from one central observation: the customer is watching a spinner while this system decides.
Chat routing is fundamentally a real-time bipartite matching problem between waiting conversations and available agents, and the system’s core job is keeping both sides’ state fresh to the second so that decisions land quickly and land on the right person.
Atomicity in the assignment step is non-negotiable — exactly-one-agent-per-conversation correctness under concurrent matching attempts is the single most important guarantee in the whole design, and every other component either supports it or defers to it.
Effective routing combines hard eligibility filters (skill, language, availability) with a secondary scoring tiebreaker (load, idle time, historical fit), and deliberately introduces bounded randomization to balance optimality against fairness. Neither pure round robin nor pure “always pick the best” is sufficient on its own.
Priority queues need aging to prevent starvation of lower-priority conversations during sustained high-priority volume; strict priority without aging quietly punishes the customers who can least afford to wait.
Graceful degradation — skill relaxation, cross-region overflow, bot deflection, async fallback — must be designed as first-class behavior for the routine condition of “no agent currently available,” not treated as an edge case.
The system decomposes cleanly by responsibility and scaling profile: connection-heavy gateways, compute-heavy classification, high-frequency in-memory presence and queue state, and durable event logging for analytics and compliance — each deserving independent scaling strategies rather than being forced into a single deployment shape.
Monitoring must track customer-experience metrics (wait time, abandonment) as leading indicators, since they often reveal problems before they show up in aggregate SLA or system-health metrics.
Key takeaways an interviewer wants to hear
- Model it as bipartite matching, not a queue with side effects. Conversations and agents are two dynamic sets, and the system exists to pair them.
- Assignment must be atomic. An atomic capacity decrement (or single-writer partitioning) is what guarantees exactly-one-agent-per-conversation.
- Combine hard filters with a scoring tiebreaker, and introduce bounded randomization so the top performer is not permanently on fire.
- Age your priority queue. Otherwise low-tier customers starve during sustained high-tier volume.
- Design overflow as a first-class chain — skill relaxation, cross-region overflow, bot deflection, async fallback — not as an edge case.
- Partition by skill or queue to scale the matching problem horizontally and isolate failures per partition.
- Keep presence in-memory and durable state in real stores. They have different consistency needs and belong in different systems.
- Instrument leading indicators (wait time, abandonment, fairness variance) so regressions surface before SLA numbers move.