Fixing 60-Second Timeouts: Batch Operations 120x Faster
The AI assistant's 'Execute Schedule' timed out creating 8 appointments. Three compounding problems -- sequential calls, synchronous Google sync, and observer loops.
The AI chat assistant's "Execute Schedule" command creates 6-8 wellness appointments at once. It was timing out at 60 seconds. After fixing three compounding problems, it completes in ~500ms.
The Chain Reaction
Each appointment creation triggered this:
Mobile: for loop -> 8 sequential API calls
Server: Create Appointment
Observer: created() fires
├── scheduleReminder() -> update() -> triggers updated() observer!
├── SyncToGoogle() -> HTTP to Google API (3-5 seconds each)
└── updated() observer fires again -> more Google sync!
Three problems compounding: sequential HTTP calls from mobile, synchronous Google Calendar sync blocking each request, and observer loops where internal update() calls triggered more observers.
Fix 1: Batch API
// BEFORE: 8 sequential requests
for (const block of blocks) {
await chatApi.executeAction({ type: 'create_appointment', params: { ... } });
}
// AFTER: single batch request
const actions = blocks.map(block => ({
type: 'create_appointment',
params: { description: block.title, date: schedule.date, time: block.time },
}));
await chatApi.executeActions(actions);
Fix 2: Async Google Sync
// BEFORE: blocks for 3-5 seconds per appointment
public function created(Appointment $appointment): void
{
$this->syncToGoogle($appointment, 'create'); // Blocking!
}
// AFTER: dispatch job, return immediately
public function created(Appointment $appointment): void
{
SyncAppointmentToGoogle::dispatch($appointment->id, 'create');
}
The queued job handles retries (3 attempts with 5/15/30s backoff) and uses withoutEvents() when saving the Google event ID back to the appointment -- preventing observer re-triggers.
Fix 3: Break Observer Loops
scheduleReminder() was calling $appointment->update(), which triggered the updated observer, which dispatched another Google sync:
// BEFORE: triggers updated() observer
$appointment->update(['reminder_job_id' => $jobId]);
// AFTER: skip observers for internal bookkeeping
Appointment::withoutEvents(function () use ($appointment, $jobId) {
$appointment->update(['reminder_job_id' => $jobId]);
});
Result
| Metric | Before | After |
|---|---|---|
| API calls | 8 sequential | 1 batch |
| Response time | 60s+ (timeout) | ~500ms |
| Google sync | Blocking | Async queued |
| Observer triggers | Cascading loops | Isolated |
The user sees instant results. Google Calendar sync happens in the background.
Built while working on PlanPal.
Back to danielships ->