Summary
Bulk Loom import via CSV cannot complete for large libraries. The per-user rate
limit empties partway through the CSV, all remaining rows fail, and — because
the rate-limit check runs before the already-imported check — retrying the same
CSV spends the entire rate-limit budget on rows that already succeeded and
imports zero new videos.
Environment
- Cap Pro (web), CSV import at /dashboard (Loom importer)
- Library: ~1,500 Loom videos, importing in CSVs under the 500-row cap
- Google Drive storage integration enabled
Steps to reproduce
- Import a CSV with more rows than the
rl_loom_import_per_user window allows
(~60 in my case).
- First ~60 rows import; every subsequent row fails with
"Too many Loom imports started. Please wait a few minutes, then try again."
- Wait, then re-submit the same CSV to pick up the failed rows.
- Result:
{"success":false,"importedCount":0, ...} — every row fails with the
same rate-limit error. No new videos import.
Cause (from source)
In apps/web/actions/loom.ts, the CSV loop calls isRateLimited() per row
before importLoomVideoForOwner(), and the duplicate check
("This Loom video has already been imported.") lives inside
importLoomVideoForOwner(). So on a retry, every already-imported row consumes
a rate-limit token at the gate and never reaches the dedupe short-circuit. For
a 500-row CSV where 60 succeeded, the retry burns the whole window on rows
1-60 plus rows that will fail anyway, and the user can never make progress
without manually trimming completed rows out of the CSV.
The loop also has no pacing or backoff, so a single large CSV always drains
the bucket and fails the tail in one shot.
Suggested fixes (any one of these unblocks large imports)
- Reorder the checks: run the dedupe lookup before
isRateLimited() so
already-imported rows return early without spending rate-limit budget.
Retrying the same CSV then becomes safe and idempotent.
- Queue instead of fail: when the limit trips mid-CSV, pause and resume
rather than failing the remaining rows (the job is already async
server-side).
- Surface the budget: return remaining quota / retry-after in the response
so the UI can tell users how many rows to submit and when to resume.
Actual response sample
{"success":false,"importedCount":0,"failedCount":10,"results":[
{"rowNumber":112,"userEmail":"…","success":false,
"error":"Too many Loom imports started. Please wait a few minutes, then try again."},
…
],"error":"No Loom videos were imported."}
Happy to test a fix — I have ~1,440 videos still to import and an active Loom
account to reproduce against.
Summary
Bulk Loom import via CSV cannot complete for large libraries. The per-user rate
limit empties partway through the CSV, all remaining rows fail, and — because
the rate-limit check runs before the already-imported check — retrying the same
CSV spends the entire rate-limit budget on rows that already succeeded and
imports zero new videos.
Environment
Steps to reproduce
rl_loom_import_per_userwindow allows(~60 in my case).
"Too many Loom imports started. Please wait a few minutes, then try again."{"success":false,"importedCount":0, ...}— every row fails with thesame rate-limit error. No new videos import.
Cause (from source)
In
apps/web/actions/loom.ts, the CSV loop callsisRateLimited()per rowbefore
importLoomVideoForOwner(), and the duplicate check(
"This Loom video has already been imported.") lives insideimportLoomVideoForOwner(). So on a retry, every already-imported row consumesa rate-limit token at the gate and never reaches the dedupe short-circuit. For
a 500-row CSV where 60 succeeded, the retry burns the whole window on rows
1-60 plus rows that will fail anyway, and the user can never make progress
without manually trimming completed rows out of the CSV.
The loop also has no pacing or backoff, so a single large CSV always drains
the bucket and fails the tail in one shot.
Suggested fixes (any one of these unblocks large imports)
isRateLimited()soalready-imported rows return early without spending rate-limit budget.
Retrying the same CSV then becomes safe and idempotent.
rather than failing the remaining rows (the job is already async
server-side).
so the UI can tell users how many rows to submit and when to resume.
Actual response sample
{"success":false,"importedCount":0,"failedCount":10,"results":[ {"rowNumber":112,"userEmail":"…","success":false, "error":"Too many Loom imports started. Please wait a few minutes, then try again."}, … ],"error":"No Loom videos were imported."}Happy to test a fix — I have ~1,440 videos still to import and an active Loom
account to reproduce against.