// Let's make `parseManifest` accept a callback that takes a `ManifestEntry` (very javascript =). // It just processes lines into manifest entries and hands them off. func parseManifest(ctx context.Context, manifestBody string, processEntry func(ManifestEntry)) error { dec := json.NewDecoder(strings.NewReader(manifestBody)) for { var e ManifestEntry if err := dec.Decode(&e); err == io.EOF { break } else if err != nil { return fmt.Errorf("failed to decode manifest entry: %w", err) } processEntry(e) } return nil } // In handler, update parseManifest // Use a waitgroup and goroutines to process each file -> csv conversion and upload separately // Something like: var wg sync.WaitGroup err := parseManifest(ctx, manifest, func(entry ManifestEntry) { wg.Add(1) go func(e ManifestEntry) { defer wg.Done() // download file and convert to csv (could potentially be separate operations ...?) csv, err := getExportDataFile(ctx, e.DataFileS3Key) if err != nil { log.Printf("Failed to get export data for %s: %v", e.DataFileS3Key, err) return // probs need to handle an issue better } // do something for filename ... filename := fmt.Sprintf("export_%s.csv", extractFileID(e.DataFileS3Key)) // preferably upload without writing to disk first ... if err := uploadCSV(filename, csv); err != nil { log.Printf("Failed to upload CSV for %s: %v", e.DataFileS3Key, err) return // probs need to handle an issue better } log.Printf("Successfully processed %s", e.DataFileS3Key) }(entry) }) wg.Wait()