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.