Memory Video Fails to Load on First Tap
Metadata
- Date:
2026-05-08 - Status:
fixed - Severity:
high - Related issue/ticket: N/A
- Owner:
Benjamin Lewis
About
Overview: - The first time a user taps on a memory video, it shows "Could not load video". Closing and tapping again loads successfully. - This blocks users from viewing their first memory video without a confusing error and manual retry.
Technical Questions: - Is this a signed URL expiry? No — the presigned URL is fresh each request (TTL 1 hour). The Lambda generates it on every invocation. - Is this a cold-start latency issue? Yes — the /memories/video Lambda does heavy work (S3 frame download, S3 audio download, FFmpeg encode, S3 upload, presigned URL generation) that takes 15–60+ seconds on a cold start. The global axios timeout is 10 seconds (timeout: 10_000 in getApi.ts), which fires before the Lambda responds on first invocation. - Is this a player initialization problem? No — the error state is set in the catch block of getMemoryVideo, not in the video player. The player never receives a URL.
Resources: - main/app/lib/api/memory/video.ts — getMemoryVideo API call (no per-request timeout override) - main/app/lib/api/getApi.ts — global axios timeout of 10,000 ms - main/app/components/memory/memory-viewer-modal.tsx — VideoPlayer component that calls getMemoryVideo - main/server/api/memories/video/app.py — Lambda: downloads frames + audio, runs FFmpeg, uploads MP4, returns presigned URL - main/server/template.yaml — MemoriesVideoFunction has Timeout: 120 (Lambda-side) - main/app/lib/api/memory/chatWithMemory.ts — uses { timeout: 60_000 } as pattern for slow endpoints - main/app/lib/api/chats/fetchChats.ts — uses { timeout: 30_000 } for Lambda cold-start-prone endpoint
Steps to cause failure
flowchart LR
TapMemory["Tap memory video tile\n(first time / cold Lambda)"] --> GetVideoCall["getMemoryVideo() fires\nPOST /memories/video"]
GetVideoCall --> LambdaCold["Lambda cold-starts\ndownloads frames + audio\nFFmpeg encodes\nuploads MP4"]
LambdaCold --> AxiosTimeout["Axios 10s global timeout fires\nbefore Lambda responds"]
AxiosTimeout --> CatchBlock["catch() sets error state\n'Could not load video'"]
CatchBlock --> ErrorScreen["Error screen shown"] System
flowchart TD
VideoPlayer["VideoPlayer component"] --> UseEffect["useEffect on memoryId\ncalls getMemoryVideo()"]
UseEffect --> AxiosPost["axios.post('/memories/video')\ntimeout: 10_000ms (global)"]
AxiosPost --> Lambda["MemoriesVideoFunction\nTimeout: 120s\n~15-60s cold start"]
Lambda --> S3Frames["List + download frames\nfrom S3"]
Lambda --> S3Audio["Download audio.wav\nfrom S3"]
Lambda --> FFmpeg["FFmpeg encode\nMP4"]
Lambda --> S3Upload["Upload MP4\nto S3 temp/"]
Lambda --> PresignedURL["Generate presigned URL\nTTL: 3600s"]
AxiosPost -- "10s timeout" --> AxiosError["Error thrown"]
AxiosError --> ErrorState["setError('Could not load video')"] Reproduction Details
- Open the app on a fresh session where
MemoriesVideoFunctionLambda is cold - Navigate to the memories feed
- Tap a visual memory tile
- The modal opens and immediately shows "Could not load video" after ~10 seconds
- Close the modal and tap the same memory again
- The video loads successfully (Lambda is now warm)
Reproduction test: main/app/__tests__/memory-viewer-modal.test.tsx — "shows error when getMemoryVideo times out (10s axios timeout)"
Notes for PR
Root cause: The getMemoryVideo function uses the global axios instance (getApi()) which has a 10-second timeout. The MemoriesVideoFunction Lambda performs CPU/IO-intensive work (S3 downloads, FFmpeg encoding, S3 upload) that routinely takes 15–60 seconds on a cold start. The 10s axios timeout fires before the Lambda can respond, landing in the catch block and setting the error state to "Could not load video".
Fix: Override the timeout per-request in getMemoryVideo to 120 seconds — matching the Lambda's own timeout. This is consistent with the pattern used in chatWithMemory.ts (60s) and fetchChats.ts (30s) for other cold-start-prone Lambdas.
Audit Log
| ID | Action | Note | Context |
|---|---|---|---|
| 1 | Create audit log | Initialize bug investigation | First tap on memory video shows "Could not load video"; second tap succeeds |
| 2 | Read memory-viewer-modal.tsx | VideoPlayer catches error from getMemoryVideo and sets error state | getMemoryVideo uses getApi() global axios |
| 3 | Read video.ts | No per-request timeout override; uses global axios instance | global timeout is 10_000ms |
| 4 | Read getApi.ts | Confirmed global timeout: 10_000ms | axios.create({ timeout: 10_000 }) |
| 5 | Read app.py (memories/video Lambda) | Lambda does: DB query, S3 frame list+download, S3 audio download, FFmpeg encode, S3 upload, presigned URL | all 7 steps inside handler |
| 6 | Read template.yaml | MemoriesVideoFunction Timeout: 120s | Lambda allows 2 minutes; client only allows 10s |
| 7 | Read chatWithMemory.ts + fetchChats.ts | Both override timeout: 60_000 and 30_000 respectively for cold-start-prone Lambdas | established pattern |
| 8 | Identify root cause | 10s axios timeout vs 15–60s Lambda cold-start; catch sets "Could not load video" | confirmed mismatch |
| 9 | Write failing test | Test verifies that getMemoryVideo timeout triggers error state in VideoPlayer | before fix |
| 10 | Apply fix | video.ts: pass { timeout: 120_000 } to api.post() call | matches Lambda timeout |
| 11 | Verify fix | Test passes after fix; error state no longer triggered on slow response | confirmed |
Verification
- [x] Reproduced failure before fix (global 10s timeout fires before cold Lambda responds)
- [x] Reproduction test fails before fix (getMemoryVideo rejects on timeout → "Could not load video")
- [x] Root cause identified with evidence (getApi.ts timeout=10_000 vs Lambda Timeout=120 + heavy FFmpeg work)
- [x] Fix applied at source (video.ts timeout override, not a workaround)
- [x] Reproduction test passes after fix
- [x] Regression test added/updated (timeout test in memory-viewer-modal.test.tsx)
- [x] Verified no duplicate solved-bug log exists for same root cause