Use a separate worker process (same codebase, different entrypoint) consuming a queue, with retries and idempotency. This avoids blocking web requests and gives you better control over concurrency and failures.