Never Leave Users In Silence: Voice Progress Updates During Processing
Table of Contents
You upload a file to a voice system. The agent says “Processing…”
Then: silence.
10 seconds. 20 seconds. 30 seconds.
Is it still working? Did it crash? Should you wait or hang up?
You have no idea.
Silence feels like abandonment. When users can’t see what’s happening, they assume nothing is happening. They get anxious. They hang up. They retry and create duplicate submissions.
Visual interfaces solve this with spinners and progress bars. Voice interfaces need to narrate progress.
Here’s how to build voice agents that never leave users wondering what’s happening.
The Silence Problem
Humans are terrible at estimating time during uncertain waits.
Psychological studies show:
- 10 seconds of unexplained silence feels like 45 seconds
- Users assume failure after ~20 seconds of silence
- Anxiety increases exponentially during silent waits
- 40% of users abandon tasks if they don’t know what’s happening
Common scenarios where silence kills UX:
File uploads:
User: "Here's the document"
Agent: "Processing your file..."
[30 seconds of silence]
User: "Hello? Are you still there?"
Payment processing:
Agent: "Processing your payment..."
[15 seconds of silence]
User: [Presses button again, creates duplicate charge]
Database queries:
Agent: "Let me look that up..."
[25 seconds of silence]
User: [Hangs up, assumes system broke]
API calls:
Agent: "Checking availability..."
[20 seconds of silence]
User: [Tries again, gets rate limited]
All preventable with progress narration.
Why Voice Needs Progress Updates More Than Visual UIs
Visual interfaces have constant feedback:
- Spinners show something’s happening
- Progress bars show how much is done
- Percentage numbers show time remaining
- UI is still visible (not “dead”)
Voice interfaces have none of this unless you explicitly narrate:
graph TD
A[Operation starts] --> B[Agent announces operation]
B --> C{Expected duration?}
C -->|<5 seconds| D[No update needed]
C -->|5-15 seconds| E[One update at midpoint]
C -->|15-30 seconds| F[Updates every 10 seconds]
C -->|>30 seconds| G[Updates every 10-15 seconds + estimated time]
D --> H[Operation completes]
E --> I[Mid-operation update]
I --> H
F --> J[First update - 10s]
J --> K[Second update - 20s]
K --> H
G --> L[Initial: estimated time]
L --> M[Update every 10-15s]
M --> N{Still processing?}
N -->|Yes| M
N -->|No| H
H --> O[Announce completion]
Key principle: If it takes longer than 5 seconds, narrate what’s happening.
Implementation: Time-Aware Progress Narration
Here’s how to build progress updates:
import { RealtimeClient } from '@openai/realtime-api-beta';
const client = new RealtimeClient({
apiKey: process.env.OPENAI_API_KEY,
model: 'gpt-realtime'
});
class ProgressNarrator {
constructor(operation, estimatedDuration) {
this.operation = operation;
this.estimatedDuration = estimatedDuration; // in seconds
this.startTime = null;
this.updateIntervals = [];
this.isComplete = false;
}
async start() {
this.startTime = Date.now();
// Announce start
await this.announce(this.getStartMessage());
// Schedule updates based on estimated duration
this.scheduleUpdates();
}
scheduleUpdates() {
if (this.estimatedDuration < 5) {
// No updates needed for quick operations
return;
} else if (this.estimatedDuration <= 15) {
// One update at midpoint
this.updateIntervals = [this.estimatedDuration / 2];
} else if (this.estimatedDuration <= 30) {
// Updates every 10 seconds
this.updateIntervals = [10, 20];
} else {
// Updates every 10-15 seconds for longer operations
const numUpdates = Math.floor(this.estimatedDuration / 12);
this.updateIntervals = Array.from(
{ length: numUpdates },
(_, i) => (i + 1) * 12
);
}
// Set timers for each update
this.updateIntervals.forEach(delay => {
setTimeout(() => {
if (!this.isComplete) {
this.announce(this.getProgressMessage());
}
}, delay * 1000);
});
}
getStartMessage() {
if (this.estimatedDuration < 10) {
return `${this.operation}, this will just take a moment...`;
} else if (this.estimatedDuration < 30) {
return `${this.operation}, this usually takes about ${this.estimatedDuration} seconds...`;
} else {
const minutes = Math.ceil(this.estimatedDuration / 60);
return `${this.operation}, this typically takes ${minutes} ${minutes === 1 ? 'minute' : 'minutes'}...`;
}
}
getProgressMessage() {
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
const remaining = this.estimatedDuration - elapsed;
const messages = [
"Still working on it...",
"Almost there...",
"Just a bit longer...",
"Processing...",
"Still going...",
`About ${Math.max(5, remaining)} more seconds...`
];
// Choose message based on progress
if (remaining <= 5) {
return "Almost done...";
} else if (elapsed < this.estimatedDuration * 0.5) {
return messages[0];
} else if (elapsed < this.estimatedDuration * 0.75) {
return messages[1];
} else {
return messages[2];
}
}
async complete(result) {
this.isComplete = true;
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
let completionMessage = "Done!";
if (elapsed > this.estimatedDuration * 1.5) {
completionMessage = "Done! Sorry that took longer than expected.";
} else if (elapsed < this.estimatedDuration * 0.5) {
completionMessage = "Done! That was quick.";
}
await this.announce(`${completionMessage} ${result}`);
}
async announce(message) {
await client.sendUserMessage({
type: 'message',
content: [{
type: 'text',
text: message
}]
});
}
}
// Usage example
async function uploadFileWithProgress(file) {
const fileSize = file.size;
const estimatedTime = calculateUploadTime(fileSize); // in seconds
const progress = new ProgressNarrator(
"I'm processing your file",
estimatedTime
);
await progress.start();
try {
const result = await processFile(file);
await progress.complete(`Your file is ready. ${result.summary}`);
} catch (error) {
progress.isComplete = true;
await client.sendUserMessage({
type: 'message',
content: [{
type: 'text',
text: `I ran into an issue: ${error.message}. Let me try a different approach.`
}]
});
}
}
function calculateUploadTime(fileSize) {
// Estimate based on file size and typical processing time
const mbSize = fileSize / (1024 * 1024);
if (mbSize < 1) return 5;
if (mbSize < 5) return 15;
if (mbSize < 20) return 30;
return 60;
}
Message Patterns By Duration
Different wait times need different narration strategies:
< 5 Seconds: No Updates Needed
Agent: "Let me check that..."
[3 seconds]
Agent: "Found it! Your balance is $150."
5-15 Seconds: One Update
Agent: "I'm processing your file, this usually takes about 10 seconds..."
[7 seconds]
Agent: "Almost there..."
[3 seconds]
Agent: "Done! Your file is ready."
15-30 Seconds: Multiple Updates
Agent: "Processing your payment, this typically takes about 20 seconds..."
[10 seconds]
Agent: "Still processing..."
[10 seconds]
Agent: "Almost done..."
[5 seconds]
Agent: "Complete! Payment confirmed."
> 30 Seconds: Frequent Updates + Options
Agent: "I'm analyzing your document, this usually takes about a minute..."
[15 seconds]
Agent: "Still working on it... About 45 more seconds."
[15 seconds]
Agent: "About halfway done..."
[15 seconds]
Agent: "Almost there, just another 15 seconds..."
[15 seconds]
Agent: "Done! Here's what I found..."
> 2 Minutes: Offer Alternatives
Agent: "This is a large file—processing might take 2-3 minutes.
Would you like me to:
1. Continue processing while we talk?
2. Send you a notification when it's done?
3. Process it and call you back?"
Real-World Examples
Example 1: Payment Processing
Without progress updates:
Agent: "Processing your payment..."
[20 seconds of silence]
User: "Hello?"
[User presses button again → duplicate charge]
With progress updates:
Agent: "Processing your payment, this usually takes about 15 seconds..."
[10 seconds]
Agent: "Still processing, almost done..."
[8 seconds]
Agent: "Payment confirmed! Your order is placed."
Impact:
- Zero duplicate submissions
- User stays confident
- Clear completion signal
Example 2: Large File Upload
Without progress updates:
Agent: "Uploading your file..."
[45 seconds of silence]
User: [Hangs up, assumes it failed]
With progress updates:
Agent: "I'm processing your file, this is a large one so it'll take about a minute..."
[15 seconds]
Agent: "About 45 seconds left..."
[15 seconds]
Agent: "Halfway there..."
[15 seconds]
Agent: "Almost done, just 15 more seconds..."
[15 seconds]
Agent: "Done! I've extracted 347 transactions from your file. Want a summary?"
Impact:
- User knows what’s happening
- No anxiety about broken systems
- Clear expectation set and met
Example 3: Database Query
Without progress updates:
Agent: "Let me look that up..."
[25 seconds of silence]
User: "Are you still there?"
Agent: "Yes, still searching..."
[User frustrated by lack of communication]
With progress updates:
Agent: "Let me search our records, this might take 20-30 seconds..."
[12 seconds]
Agent: "Still searching, checking multiple sources..."
[12 seconds]
Agent: "Almost got it..."
[8 seconds]
Agent: "Found it! You have 3 past orders..."
Impact:
- Anxiety eliminated
- User trusts system is working
- Perceived wait time shorter
Advanced Patterns
1. Adaptive Timing Based On Actual Progress
class AdaptiveProgressNarrator {
constructor(operation) {
this.operation = operation;
this.startTime = Date.now();
this.lastUpdateTime = Date.now();
this.minUpdateInterval = 8000; // 8 seconds minimum between updates
}
async update(percentComplete) {
const now = Date.now();
const timeSinceUpdate = now - this.lastUpdateTime;
// Only update if enough time has passed
if (timeSinceUpdate < this.minUpdateInterval) {
return;
}
this.lastUpdateTime = now;
let message = '';
if (percentComplete < 25) {
message = 'Still working on it...';
} else if (percentComplete < 50) {
message = 'About a quarter done...';
} else if (percentComplete < 75) {
message = 'Halfway there...';
} else if (percentComplete < 90) {
message = 'Almost finished...';
} else {
message = 'Just a few more seconds...';
}
await this.announce(message);
}
}
2. Context-Aware Updates
Different operations need different messaging:
const OperationMessages = {
file_upload: {
start: "Uploading your file...",
progress: "Still uploading...",
complete: "Upload complete!"
},
payment: {
start: "Processing your payment...",
progress: "Confirming with your bank...",
complete: "Payment confirmed!"
},
analysis: {
start: "Analyzing your data...",
progress: "Finding patterns...",
complete: "Analysis complete!"
},
search: {
start: "Searching our records...",
progress: "Checking multiple sources...",
complete: "Found results!"
}
};
function getContextualMessage(operationType, stage) {
return OperationMessages[operationType][stage];
}
3. Graceful Timeout Handling
What if operation takes way longer than expected?
async function handleLongOperation(operation, maxWaitTime = 120) {
const progress = new ProgressNarrator(operation.name, operation.estimatedTime);
await progress.start();
const timeout = setTimeout(async () => {
if (!progress.isComplete) {
await client.sendUserMessage({
type: 'message',
content: [{
type: 'text',
text: `This is taking longer than expected. I can:
1. Keep waiting (might be a few more minutes)
2. Cancel and try again later
3. Transfer you to someone who can help
What would you prefer?`
}]
});
}
}, maxWaitTime * 1000);
try {
const result = await operation.execute();
clearTimeout(timeout);
await progress.complete(result);
} catch (error) {
clearTimeout(timeout);
await handleError(error);
}
}
4. Background Processing With Callbacks
For very long operations, offer to notify user later:
async function offerCallback(operation, estimatedMinutes) {
await client.sendUserMessage({
type: 'message',
content: [{
type: 'text',
text: `This will take about ${estimatedMinutes} minutes. I can:
1. Process it now while we stay on the line
2. Process it in the background and text you when it's done
3. Process it and call you back
Which works better for you?`
}]
});
}
async function processInBackground(operation, notifyMethod) {
// Start async processing
const jobId = await operation.startAsync();
await client.sendUserMessage({
type: 'message',
content: [{
type: 'text',
text: `Got it! I'll ${notifyMethod === 'text' ? 'text' : 'call'} you when it's done.
Your reference number is ${jobId}. Anything else I can help with right now?`
}]
});
// Monitor job and notify when complete
monitorJobAndNotify(jobId, notifyMethod);
}
Business Impact: Real Numbers
A financial services company added progress narration to payment processing:
Before (silent processing):
- 18% duplicate submission rate (users resubmitted thinking it failed)
- 12% abandonment during processing
- Average time-to-anxiety: 14 seconds
- CSAT: 3.1/5 for payment experience
After (with progress updates):
- 2% duplicate submission rate (89% reduction)
- 3% abandonment (75% reduction)
- No anxiety reports during processing
- CSAT: 4.3/5 (39% improvement)
Cost impact:
- Duplicate submissions created $87,000/month in refund processing costs
- Reduction saved $77,000/month
- Support tickets about “payment status” dropped 64%
Design Guidelines
1. Always Set Expectations Before starting: Tell user how long it will take
"This usually takes about 30 seconds..."
2. Update Based On Duration
- <5s: No updates
- 5-15s: One update
- 15-30s: 2-3 updates
30s: Every 10-15 seconds
3. Vary Your Language Don’t repeat “Still processing…” robotically
"Still working on it..."
"Almost there..."
"Just a bit longer..."
"Nearly finished..."
4. Acknowledge Long Waits If it takes longer than expected, apologize:
"Sorry this is taking longer than usual..."
5. Celebrate Completion Clear signal when done:
"Done!" or "All set!" or "Complete!"
6. Provide Options For Very Long Waits After 60-90 seconds, offer alternatives:
"This is taking a while. Want me to call you back when it's done?"
Testing Progress Updates
Metrics to track:
Abandonment During Processing:
- % of users who hang up mid-operation
- Target: <5%
Duplicate Submissions:
- Users who retry thinking it failed
- Target: <3%
Time-To-Anxiety:
- How long before users express concern
- Target: >30 seconds (or never)
Perceived Wait Time:
- Ask users: “How long did that feel?”
- With good updates: Perceived time ~= actual time
- Without updates: Perceived time 2-3x actual time
Completion Confidence:
- Do users trust operation completed successfully?
- Target: >95% confidence without asking for confirmation
Implementation Checklist
- Identify all operations >5 seconds
- Measure typical duration for each operation
- Write context-appropriate progress messages
- Implement time-based update scheduling
- Add completion announcements
- Handle timeout scenarios
- Test with real users
- Measure abandonment rates
- Track duplicate submission rates
- Monitor user sentiment during waits
Edge Cases To Handle
1. Operation Completes Faster Than Expected Don’t give unnecessary updates:
if (operation.isComplete && !hasGivenUpdate) {
// Skip update, jump straight to completion
await announceCompletion();
}
2. Operation Fails Mid-Process Update user immediately:
"I ran into an issue: [error]. Let me try again..."
3. User Interrupts During Processing Handle gracefully:
User: "Cancel that"
Agent: "Canceling... Done. What would you like instead?"
4. Multiple Concurrent Operations Narrate both:
"I'm processing your payment and sending your receipt. The payment is almost done...
Payment complete! Receipt sent."
The Psychology Of Waiting
Research shows narrated waits feel shorter:
Silent 20-second wait: Feels like 40-60 seconds
Narrated 20-second wait: Feels like 20-25 seconds
Why?
- Updates reduce uncertainty (anxiety makes time crawl)
- Progress signals trigger dopamine (brain perceives movement)
- Narration occupies attention (distracted minds don’t count seconds)
Voice agents that narrate progress don’t just inform—they reassure.
“I’m still here. I’m working on it. Almost done.”
That’s all users need to hear to stay calm and confident.
The Bottom Line
Silence is abandonment. In voice interfaces, if users can’t hear what’s happening, they assume nothing’s happening.
Every operation longer than 5 seconds needs narration:
- Set expectations upfront
- Provide progress updates
- Signal completion clearly
This isn’t complexity—it’s courtesy.
Users shouldn’t have to wonder if your system is working. Tell them.
If you want voice agents that narrate progress and never leave users in anxious silence, we can add time-aware status updates to your OpenAI Realtime API integration. Turn silent waits into narrated progress.