Last updated: March 1, 2025

<span aria-hidden="true" id="resumable-file-uploads-in-angular-with-rxjs"></span>

# Resumable file uploads in Angular with RxJS

![Tim Koschützki](/assets/images/teammates/avatar-tim-kos-1.jpg?dpl=dpl_C6YH6XrtnwbHLJcm1LDKywQn4CB3)

#### Tim Koschützki

Co-founder · Berlin, Germany · Show bio

[](https://x.com/tim%5Fkos)[](https://github.com/tim-kos)

Uploading large files can be challenging due to network interruptions, browser limitations, and user cancellations. Chunked and resumable uploads provide a robust solution by breaking files into smaller, manageable pieces and allowing uploads to resume after interruptions.

<span aria-hidden="true" id="why-chunked-and-resumable-uploads"></span>

## Why chunked and resumable uploads?

Chunked uploads split large files into smaller segments, reducing memory usage and improving reliability. Resumable uploads allow interrupted uploads to continue from the last successful chunk, enhancing the user experience.

<span aria-hidden="true" id="setting-up-your-angular-project"></span>

## Setting up your Angular project

First, ensure you have the latest Angular CLI installed:

```bash
# Install the latest Angular CLI globally
npm install -g @angular/cli

# Create a new Angular project
ng new chunked-upload-app
cd chunked-upload-app

```

RxJS is bundled with Angular by default, so no separate installation is needed.

For Angular applications using the traditional NgModule approach, ensure `HttpClientModule` is imported in your `app.module.ts`:

```typescript
import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { HttpClientModule } from '@angular/common/http'
import { AppComponent } from './app.component'

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule],
  bootstrap: [AppComponent],
})
export class AppModule {}

```

For standalone applications (Angular 15+), use the `provideHttpClient` function in your`app.config.ts`:

```typescript
import { ApplicationConfig } from '@angular/core'
import { provideHttpClient } from '@angular/common/http'

export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient()],
}

```

<span aria-hidden="true" id="implementing-file-slicing"></span>

## Implementing file slicing

Use the HTML5 File API to slice files into chunks, with error handling:

```typescript
sliceFile(file: File, chunkSize: number): Blob[] {
  const chunks: Blob[] = [];
  let offset = 0;

  try {
    while (offset < file.size) {
      const chunk = file.slice(offset, offset + chunkSize);
      chunks.push(chunk);
      offset += chunkSize;
    }
    return chunks;
  } catch (error) {
    console.error('Error slicing file:', error);
    throw new Error('Failed to slice file. Your browser may not support the File API.');
  }
}

```

<span aria-hidden="true" id="creating-an-rxjs-pipeline-for-chunk-uploads"></span>

## Creating an RxJS pipeline for chunk uploads

Here's an implementation for sequentially uploading chunks using RxJS, with error handling:

```typescript
import { HttpClient, HttpHeaders, HttpEventType, HttpEvent } from '@angular/common/http';
import { concatMap, catchError, tap, filter, map } from 'rxjs/operators';
import { from, Observable, throwError } from 'rxjs';

uploadChunks(chunks: Blob[], uploadUrl: string, fileId: string): Observable<HttpEvent<any>> {
  return from(chunks).pipe(
    concatMap((chunk, index) => {
      const headers = new HttpHeaders({
        'Content-Range': `bytes ${index * chunk.size}-${(index + 1) * chunk.size - 1}/${chunks.length * chunk.size}`,
        'X-File-Id': fileId
      });

      return this.http.post(uploadUrl, chunk, {
        headers,
        reportProgress: true,
        observe: 'events'
      }).pipe(
        tap(event => {
          if (event.type === HttpEventType.Response) {
            console.log(`Chunk ${index} uploaded successfully`);
          }
        }),
        catchError(error => {
          console.error(`Error uploading chunk ${index}:`, error);
          return throwError(() => new Error(`Failed to upload chunk ${index}: ${error.message}`));
        })
      );
    })
  );
}

```

<span aria-hidden="true" id="adding-pause-resume-and-cancellation"></span>

## Adding pause, resume, and cancellation

Implement a service for handling pause, resume, and cancellation with state tracking:

```typescript
import { Injectable } from '@angular/core'
import { HttpClient, HttpEvent, HttpEventType, HttpHeaders } from '@angular/common/http'
import { Observable, Subject, from, throwError } from 'rxjs'
import { concatMap, takeUntil, catchError, tap, switchMap, startWith } from 'rxjs/operators'

export interface FileUploadProgress {
  id: string
  file: File
  progress: number
  complete: boolean
  error: boolean
  errorMessage?: string
}

@Injectable({
  providedIn: 'root',
})
export class FileUploadService {
  private cancel$ = new Subject<void>()
  private pause$ = new Subject<void>()
  private resume$ = new Subject<void>()
  private uploadedChunks = new Map<string, number>() // fileId -> lastUploadedChunkIndex

  constructor(private http: HttpClient) {}

  uploadFile(
    file: File,
    uploadUrl: string,
    chunkSize: number = 1024 * 1024,
  ): Observable<FileUploadProgress> {
    const fileId = this.generateId()
    const chunks = this.sliceFile(file, chunkSize)
    this.uploadedChunks.set(fileId, 0)

    return this.uploadChunksWithResume(chunks, uploadUrl, fileId).pipe(
      filter(
        (event) =>
          event.type === HttpEventType.UploadProgress || event.type === HttpEventType.Response,
      ),
      map((event) => {
        let progress = 0
        let complete = false
        let error = false

        if (event.type === HttpEventType.UploadProgress && event.total) {
          progress = Math.round((100 * event.loaded) / event.total)
        } else if (event.type === HttpEventType.Response) {
          progress = 100
          complete = true
        }

        return {
          id: fileId,
          file,
          progress,
          complete,
          error,
        } as FileUploadProgress
      }),
      catchError((err) => {
        return throwError(
          () =>
            ({
              id: fileId,
              file,
              progress: 0,
              complete: false,
              error: true,
              errorMessage: err.message,
            }) as FileUploadProgress,
        )
      }),
    )
  }

  private uploadChunksWithResume(
    chunks: Blob[],
    uploadUrl: string,
    fileId: string,
  ): Observable<HttpEvent<any>> {
    return this.resume$.pipe(
      startWith(null), // Start immediately
      switchMap(() => {
        const startFrom = this.uploadedChunks.get(fileId) || 0
        const remainingChunks = chunks.slice(startFrom)

        return from(remainingChunks).pipe(
          concatMap((chunk, index) => {
            const actualIndex = startFrom + index
            const headers = new HttpHeaders({
              'Content-Range': `bytes ${actualIndex * chunk.size}-${(actualIndex + 1) * chunk.size - 1}/${chunks.length * chunk.size}`,
              'X-File-Id': fileId,
              'X-Chunk-Index': actualIndex.toString(),
            })

            return this.http
              .post(uploadUrl, chunk, {
                headers,
                reportProgress: true,
                observe: 'events',
              })
              .pipe(
                tap((event) => {
                  if (event.type === HttpEventType.Response) {
                    this.uploadedChunks.set(fileId, actualIndex + 1)
                  }
                }),
                catchError((error) => {
                  console.error(`Error uploading chunk ${actualIndex}:`, error)
                  return throwError(
                    () => new Error(`Failed to upload chunk ${actualIndex}: ${error.message}`),
                  )
                }),
              )
          }),
          takeUntil(this.pause$),
          takeUntil(this.cancel$),
        )
      }),
    )
  }

  private sliceFile(file: File, chunkSize: number): Blob[] {
    const chunks: Blob[] = []
    let offset = 0

    try {
      while (offset < file.size) {
        const chunk = file.slice(offset, offset + chunkSize)
        chunks.push(chunk)
        offset += chunkSize
      }
      return chunks
    } catch (error) {
      console.error('Error slicing file:', error)
      throw new Error('Failed to slice file. Your browser may not support the File API.')
    }
  }

  private generateId(): string {
    return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
  }

  pauseUpload(): void {
    this.pause$.next()
  }

  resumeUpload(): void {
    this.resume$.next()
  }

  cancelUpload(): void {
    this.cancel$.next()
    this.uploadedChunks.clear()
  }
}

```

<span aria-hidden="true" id="tracking-progress"></span>

## Tracking progress

Use type guards for tracking upload progress with Angular's HttpClient:

```typescript
import { Component } from '@angular/core'
import { FileUploadService, FileUploadProgress } from './file-upload.service'

@Component({
  selector: 'app-file-upload',
  template: `
    <div class="upload-container">
      <input type="file" (change)="onFileSelected($event)" />
      <div *ngIf="currentUpload">
        <div class="progress-bar" [style.width.%]="currentUpload.progress"></div>
        <div class="controls">
          <button (click)="pauseUpload()" [disabled]="!currentUpload || currentUpload.complete">
            Pause
          </button>
          <button (click)="resumeUpload()" [disabled]="!currentUpload || currentUpload.complete">
            Resume
          </button>
          <button (click)="cancelUpload()" [disabled]="!currentUpload || currentUpload.complete">
            Cancel
          </button>
        </div>
        <div *ngIf="currentUpload.error" class="error">
          {{ currentUpload.errorMessage }}
        </div>
      </div>
    </div>
  `,
  styles: [
    `
      .progress-bar {
        height: 20px;
        background-color: #4caf50;
        transition: width 0.3s;
      }
      .controls {
        margin-top: 10px;
      }
      .error {
        color: red;
        margin-top: 10px;
      }
    `,
  ],
})
export class FileUploadComponent {
  currentUpload: FileUploadProgress | null = null

  constructor(private uploadService: FileUploadService) {}

  onFileSelected(event: Event): void {
    const input = event.target as HTMLInputElement
    if (input.files && input.files.length > 0) {
      const file = input.files[0]
      this.uploadFile(file)
    }
  }

  uploadFile(file: File): void {
    // Determine optimal chunk size based on file size
    const chunkSize = this.determineOptimalChunkSize(file.size)

    this.uploadService
      .uploadFile(file, 'https://your-upload-endpoint.com/upload', chunkSize)
      .subscribe({
        next: (progress) => {
          this.currentUpload = progress
        },
        error: (err) => {
          console.error('Upload failed:', err)
        },
      })
  }

  pauseUpload(): void {
    this.uploadService.pauseUpload()
  }

  resumeUpload(): void {
    this.uploadService.resumeUpload()
  }

  cancelUpload(): void {
    this.uploadService.cancelUpload()
    this.currentUpload = null
  }

  /**
   * Determines the optimal chunk size based on network conditions and file size.
   *
   * - Small chunks (< 1MB): Better for unreliable connections, but more HTTP overhead.
   * - Medium chunks (1-5MB): Good balance for most web applications.
   * - Large chunks (> 5MB): Better for reliable, high-bandwidth connections and larger files.
   */
  determineOptimalChunkSize(fileSize: number): number {
    // Base chunk size on file size
    if (fileSize < 1024 * 1024 * 10) {
      // < 10MB
      return 512 * 1024 // 512KB chunks
    } else if (fileSize < 1024 * 1024 * 100) {
      // < 100MB
      return 2 * 1024 * 1024 // 2MB chunks
    } else {
      return 5 * 1024 * 1024 // 5MB chunks
    }
  }
}

```

This DevTip demonstrates how to implement resumable, chunked file uploads in Angular using RxJS and HttpClient. For more advanced features and robust file uploading, consider using[Uppy⁠](https://uppy.io), an open-source file uploader that integrates seamlessly with Transloadit.

\#angular#rxjs#file-upload#chunked-file-upload#resumable-upload#handling-uploads-service

### 👩‍💻 Join 20k+ developers

Sign up for our [monthly newsletter](/newsletters.md) to receive direct links to 3 exclusive tech — and 2 product updates. No less, no more.

Your email:

Get access

## File uploading and encoding. Made simple.

Transloadit streamlines file handling for developers, trusted by brands like Coursera and The New York Times. We’re known for a reliable API, top-notch support, and a strong commitment to open source, with projects like [Uppy⁠](https://uppy.io) and [Tus⁠](https://tus.io) setting standards in file processing.

[Sign up](/c/)[Book a Demo](https://survey.typeform.com/to/kRg47Xi5)

No credit card needed · 5 GB included in the free plan

Cancel anytime
