Nine seconds of silence. That's how long a caller waited after asking our AI assistant a simple question like "What's the TB test process?" long enough that most people would hang up, assuming the call had dropped.
We got that down to about 1.5 seconds. This is the story of how a straightforward platform-integration task turned into designing an in-house Voice AI architecture from scratch and the latency problem that became the real engineering challenge.
Quick summary:
The project began as a task to integrate VAPI, a Voice AI platform for building conversational phone agents, into our existing system. The goal: automate user onboarding, so callers could get answers and guidance from an AI assistant instead of waiting for a human agent.
The integration went well. Then the client asked for something VAPI couldn't easily give us β a more natural, expressive voice β and one workaround led to another, until it became clear we'd eventually need to build the platform ourselves.
The project took an interesting turn when the client requested additional customizations.
One of the major requirements was a more natural and expressive voice capable of conveying emotions during conversations. At that time, fulfilling these requirements directly through VAPI was not as straightforward as we needed.
To address this, I implemented a temporary solution. I retrieved available voices from Cartesia, stored their voice IDs in a JSON configuration, and integrated Cartesia directly during the voice selection process.
This was only one example of the customizations the client wanted.
As the number of customization requests increased, it became clear that relying entirely on a third-party platform would eventually limit our flexibility. Like many developers experience when working with managed platforms or pre-built solutions, customization becomes increasingly difficult as requirements become more specific.
That led to a simple question:
Why don't we build our own Voice AI platform?
Once the idea was proposed, the first step was estimating the cost and evaluating the technologies required.
The first platform that came to mind was Twilio. Whenever telephony and phone systems are discussed, Twilio is usually one of the first names that appears in the conversation.
However, telephony was only one piece of the puzzle.
Voice AI systems must handle challenges such as:
While researching these areas, I explored Deepgram, which is widely used for Speech-to-Text (STT) processing and converts raw audio into text using deep learning models.
As I continued investigating, I realized that platforms such as Twilio, Deepgram, and Cartesia collectively provided many of the capabilities that VAPI offered.
This led to another important decision.
Should we simply use one platform? Not really. If we relied entirely on a single platform, we would be back to the same integration approach we had already implemented with VAPI.
Should we combine multiple platforms? That brought us back to the original question: cost.
I created a cost estimation document comparing different combinations and presented the options to the client. Interestingly, the client chose the combination that could be described as the "best of the best":
The goal was to leverage each platform for what it does best while maintaining full control over the orchestration layer ourselves.
Initially, I assumed this would be another integration project.
That assumption changed quickly.
My lead pointed out that integrating three major platforms into a unified Voice AI system was not a simple task. The project would require designing an architecture capable of coordinating multiple real-time services while maintaining a natural conversational experience.
As I explored the requirements further, one challenge stood out immediately: latency.
Voice conversations are highly sensitive to delays. Even a few seconds of waiting can make an AI assistant feel slow, unnatural, or broken.
The second major challenge was retrieving company-specific information efficiently through a RAG (Retrieval-Augmented Generation) pipeline.
I started by designing the overall architecture.
The first decision was that Twilio would never communicate directly with Deepgram or Cartesia. Instead, our in-house Voice AI platform would act as the central orchestrator for the entire conversation.
For every phone call, Twilio establishes a bidirectional media stream with our Voice AI server using WebSockets. Audio flows in both directions in real time.
A simple way to think about it: Twilio acts as the phone line, while our Voice AI server becomes the intelligence layer that coordinates the entire pipeline.
The pipeline looks like this:
Deepgram β LLM β Cartesia
The system also needed the ability to:
The call flow works roughly as follows:
Once the transcript arrives, the system determines which path the conversation should follow β casual conversation, company-related questions, or appointment-related requests.
For general conversation, the transcript and conversation history are sent directly to the LLM. For company-related queries, relevant information is first retrieved through our RAG service before being sent to the LLM. For appointment-related interactions, appointment-specific context is gathered and included before generating a response.
Since the system needed access to company knowledge, implementing a RAG pipeline became necessary.
The overall process was straightforward:
This allows the AI assistant to answer questions using company-specific information instead of relying solely on its general training data.
While the implementation concept sounds simple, making retrieval fast enough for real-time voice conversations introduced its own set of challenges. And that is where the real latency optimization journey began.
Voice conversations are different from normal applications.
A web application can take a few seconds to load something, and the user might tolerate it. A phone conversation is different. When a caller stops speaking, they expect the AI to respond almost immediately.
The number that matters most is time-to-first-audio: how long the caller waits before hearing the first part of the AI's response.
Our initial pipeline was essentially sequential:
Caller stops talking
β
Wait for final transcript
β
Generate query embedding
β
Search vector database
β
Wait for the complete LLM response
β
Send response to TTS
β
Start playback
Each individual step seemed reasonable. Together, they made the assistant feel slow.
A typical knowledge-based request looked roughly like this:
Individually, none of these numbers looked terrible. Together, they could result in roughly nine seconds of silence after a question such as "What's the TB test process?"
And nine seconds of silence on a phone call feels much longer than nine seconds on a webpage. That became the real challenge.
The design decision that changed the system was simple: we stopped optimizing for the total time required to generate the response and started optimizing for time-to-first-audio.
Consider two scenarios. In the first, the AI generates a four-second response but starts speaking after 1.5 seconds. In the second, it generates a two-second response but doesn't start speaking until four seconds later.
The first one feels significantly faster. The caller doesn't care that the AI is still generating the rest of its answer while it is already speaking. What matters is that the conversation continues naturally.
This changed the way I thought about the entire pipeline.
RAG wants more context. Voice wants less waiting.
If we retrieve information, wait for the complete LLM response, and only then generate the speech, the knowledge retrieval path becomes one of the slowest parts of the conversation.
And not every conversation needs RAG. A caller saying "Hi, how are you?" shouldn't trigger a vector search. Neither should "Okay." or "Thanks, that's all."
So the live pipeline became more intentional β the routing decision from earlier now determines whether retrieval even happens at all.
The important idea here: RAG is a branch of the conversation, not the default path. That was both a latency optimization and a quality improvement.
The first implementation treated every conversation turn almost like a batch process:
Retrieve β Generate β Speak
The caller wouldn't hear anything until the final step. That doesn't work well for voice.
For knowledge-based questions, retrieval still needs to happen before the grounded answer can be generated. But once the relevant context is available, the LLM can start streaming its response.
Instead of waiting for the entire answer, we buffer the generated text until we have a complete sentence. As soon as that sentence is available, it is sent to Cartesia through the live connection.
So instead of waiting for the full answer β "The TB test is required before your onboarding process can be completed. You can complete it atβ¦" β the system can begin speaking "The TB test is required beforeβ¦" while the rest of the answer is still being generated.
The pipeline therefore became something closer to:
Twilio audio
β
Deepgram
β
Partial transcripts + end-of-turn
β
Cheap router
β
βββββββββββββββββ
β Small talk β
β Knowledge β
β Tools β
βββββββββββββββββ
β
Retrieve context when required
β
LLM streaming
β
Sentence buffer
β
Cartesia
β
Twilio
The "cheap router" is a fast, lightweight classifier that looks at the transcript and decides which path the conversation should take β small talk, knowledge question, or tool call β before anything expensive like retrieval or the LLM gets involved. It has to be fast and low-cost since it sits in the critical path before every single response.
The three major components β Deepgram, the LLM, and Cartesia β were no longer simply running one after another. They started overlapping. That distinction made a huge difference.
This might sound like a small implementation detail. It wasn't.
The first version was paying connection setup costs repeatedly. The LLM connection could become idle and require another connection setup. Text-to-speech was also opening a new request for individual responses.
Those delays might not be obvious in a local development environment. They become much more obvious when you're having an actual phone conversation.
We changed the connection strategy. Connections to the LLM stay warm across turns, allowing subsequent requests to reuse an existing session rather than repeatedly paying connection setup costs. For Cartesia, we use a persistent WebSocket for the lifetime of the call rather than creating a new HTTPS request for every sentence. The server also maintains a small pool of warm sockets so that the initial greeting doesn't have to pay the full connection setup cost while the caller is already waiting.
This is not the most exciting part of building a Voice AI system. But these small delays add up. Sometimes the difference between a system that feels instant and one that feels slightly sluggish is hidden in hundreds of milliseconds that nobody notices individually.
A live onboarding call contains much more than knowledge questions. A caller might say "Hi, how are you?" or "Okay." or "Can I book Thursday at 2?" None of these should automatically trigger a vector search.
Only factual, company-specific questions should enter the retrieval path. This improves latency, but it also improves response quality. Running RAG for "Thanks." could retrieve unrelated company information and provide unnecessary context to the LLM. Likewise, an appointment request shouldn't make the model search through company documents when what it actually needs is live appointment information.
Again, the important architectural principle is: retrieval is conditional.
Another interesting optimization came from looking at when we generated query embeddings.
Initially, embedding started only after speech-to-text confirmed that the caller had finished speaking. That meant we were already paying the end-of-turn latency before even beginning the embedding step.
But during a real conversation, we receive partial transcripts while the caller is still speaking. Those partial transcripts can be useful. Once enough meaningful words are available, we can speculatively generate the embedding from the current hypothesis. By the time the end-of-turn signal arrives, the embedding may already be available.
We also introduced caching for embeddings. Filler words such as "um" and "uh" don't meaningfully change the query, so they can be removed before generating the cache key. That means questions such as "um what is onboarding" and "what is onboarding" can share the same cached embedding.
The vector search itself was never the biggest bottleneck. The embedding step was. For live retrieval, we eventually moved query embedding on-box using a small local model, bringing that part of the process down to roughly 10β30 milliseconds, instead of introducing another cloud round trip into the critical path.
Voice answers should generally be concise.
Sending huge document chunks into the LLM doesn't just increase the amount of information the model has to process β it can also make the assistant sound unnatural. Imagine calling a company and asking a simple question, only for the AI to start reading an entire policy document back to you. That's not a good voice experience.
So we keep the retrieved context intentionally small. We used a small top-k, limit the size of retrieved chunks, and use a faster model for spoken knowledge responses where appropriate. We also used a similarity threshold β if the retrieved information isn't sufficiently relevant, we don't force it into the prompt. Sometimes "I don't have that detail. I can connect you with someone on the team." is much better than a slow or potentially incorrect answer.
This is one of the biggest differences between RAG in a chatbot and RAG in a voice system. In a text interface, additional context is relatively cheap. In a phone conversation, additional context can directly affect how long the caller waits before hearing the first word.
There is another problem with RAG in Voice AI that isn't immediately obvious: the query isn't typed by a user, it's produced from speech. That means the retrieval system has to deal with transcription errors and conversational context.
Imagine the caller says "How long does that take?" That sentence by itself isn't very useful as a search query. But perhaps the previous conversation was about a TB test β the real question is "How long does the TB test take?"
Speech recognition can introduce another problem too. A caller might say "TB test" and the transcription might produce "TV test." Sending that raw transcript directly into vector search can reduce retrieval quality.
Instead of adding another expensive LLM call just to rewrite the query, we introduced a lightweight rewrite layer. It can:
For example, "How long does it take?" can become something closer to "How long does the TB test take?" The important part is that this happens without adding another expensive model call to the critical path.
Another architectural decision was separating knowledge ingestion from live retrieval. These are two completely different workloads. Document ingestion can take time. A phone call cannot.
When a document is uploaded, we can:
That work happens during ingestion rather than while someone is waiting on a phone call. The live retrieval path should be much simpler:
User query
β
Query embedding
β
Vector search
β
Small context block
β
LLM
β
Streaming response
The phone call should never have to wait for PDF parsing, document chunking, or other ingestion work. This separation is what makes RAG practical for a real-time voice system.
Assistant scoping is important here as well. We don't search the entire company's knowledge base for every question β we search the documents associated with the specific assistant handling the call. Apart from being a correctness issue, searching unnecessary data also means doing unnecessary work.
Once the basic pipeline was working, several problems became apparent.
Turn detection. If the AI waits too long after a caller s, the conversation feels slow. If it responds too quickly, it can interrupt the caller. We use conversational speech-to-text with an end-of-turn signal, along with an eager end-of-turn mechanism that allows the system to begin responding slightly earlier β saving hundreds of milliseconds. But it also introduces another problem: the same utterance can sometimes arrive more than once, with the second transcript containing additional words. The orchestrator therefore needs to understand whether the new event is the same utterance, a continuation, or a completely new question. Otherwise, the assistant can end up answering the same question twice.
Barge-in. Real people interrupt. When the caller starts speaking while the AI is talking, the system needs to stop the current response. We abort the current LLM turn, cancel in-flight speech, and instruct Twilio to discard leftover audio. But we don't tear down the Cartesia connection β reconnecting the TTS connection during a call would introduce another latency problem. After an interruption, a short backoff also prevents the pipeline from constantly starting and stopping if the caller and assistant talk over each other.
Backchannels. Then there are the small things people naturally say while listening β "Okay.", "Mm-hmm.", "Got it." These aren't necessarily interruptions. But "No." or "Stop." might be. The system therefore needs to distinguish between conversational backchannels and actual interruptions.
These details aren't particularly impressive in an architecture diagram. But they're the difference between a Voice AI demo and something that people can actually use on a phone call.
Not every interaction is a knowledge question. Appointment booking, lookups, rescheduling, cancellations, and similar actions are handled through tools connected to our existing backend. Transfers also follow routing rules β an emergency can require a human agent, while another conversation may need to be transferred to a different AI assistant.
But tool calls can take time. If the caller hears complete silence while the backend checks appointment availability, the system feels like it has stopped working. So the assistant can provide a short conversational filler while the tool is executing and then continue with the actual result. The filler isn't replacing the result β it's simply covering the time required for the backend operation.
Transfer intent is also handled with latency in mind. Simple phrase matching can be checked first because it is inexpensive. Semantic similarity can then be used when the wording is less obvious. Some of the data required for these decisions can also be prepared earlier in the call so that the first meaningful turn doesn't have to pay the entire cost.
Voice AI isn't only about getting an answer from an LLM. The system also needs guardrails. Some instructions are handled through the system prompt. Other patterns can be detected locally to block or modify unsafe inputs and outputs.
The important part is that these checks need to fit into the same real-time pipeline. A guardrail that takes long enough to create an obvious becomes another latency problem.
This is an important distinction. When I say we reduced latency from roughly 9 seconds to around 1.5 seconds, I'm referring to time-to-first-audio on a typical knowledge turn. It assumes a warm path where:
It does not mean that every possible Voice AI interaction completes in 1.5 seconds. For example, a cold appointment-booking request may require checking availability, confirming information, calling backend services, and generating multiple time slots β that's a different latency measurement.
It also isn't the total duration required to speak the entire answer. Those are separate metrics.
The important change was the experience the caller actually perceived. Instead of waiting in silence after asking a question, the assistant could begin responding while the remaining work continued in the background. The conversation started feeling like a conversation again.
Of course, none of these optimizations came for free.
Streaming the first sentence means that the system can sometimes begin speaking before the complete answer is known. We mitigate this with shorter responses and retrieval confidence thresholds rather than waiting for the entire response.
Local embeddings are significantly faster, but they need to remain aligned with the embeddings generated during document ingestion.
Heuristic query rewriting works extremely well for the specific domain we are dealing with, but it requires maintenance and won't necessarily generalize perfectly to every industry.
And perhaps the biggest tradeoff is the one we started this entire journey with: once you stop relying entirely on a managed Voice AI platform, you own the entire orchestration layer. That means dealing with:
That's the real cost of building your own Voice AI system.
The biggest lesson from this project wasn't really about Twilio, Deepgram, Cartesia, RAG, or even the LLM. Those components are replaceable.
The real product is the orchestration loop that decides:
And most importantly: how quickly can I make the caller hear something useful?
That is what ultimately determines whether a Voice AI system feels like a real conversation β or just an API pipeline talking over a phone call.
If you're building or evaluating Voice AI β whether that's sticking with a managed platform or owning the orchestration layer yourself β I'd genuinely like to compare notes. Feel free to reach out or drop a comment with what you're running into.
Mehar Aziz is a Software Engineer working on full-stack development including AI/ML. Find me on [LinkedIn].