signalapp/Signal-Server · error · ClientErrorException

Media quota exhausted

Error message

Media quota exhausted

What it means

ArchiveController.copyMedia throws this (HTTP 413) when BackupManager reports OUT_OF_QUOTA: copying a media object from the attachments CDN to the backup CDN would exceed the account's backup media storage quota. The server refuses the copy before consuming storage.

Solutions

  1. Delete unneeded backed-up media via the delete-media endpoint to free quota, then retry the copy
  2. Check remaining quota before issuing copy batches and chunk requests to fit within it
  3. Surface the 413 to the user so they can upgrade their backup plan or clear old media
  4. Do not blindly retry: the error is deterministic until storage is freed

Example fix

// before
copyAllMedia(items); // throws 413 when quota is exhausted
// after
long remaining = fetchRemainingQuota();
List<CopyMediaRequest> fitting = items.stream()
    .filter(i -> i.uploadLength() <= remaining)
    .toList();
if (fitting.size() < items.size()) promptUserToFreeQuota();
copyAllMedia(fitting);
Defensive patterns

Strategy: validation

Validate before calling

long totalAfterCopy = currentBackupBytes + items.stream().mapToLong(CopyMediaRequest::uploadLength).sum();
if (totalAfterCopy > quotaLimit) { freeSpaceOrNotifyUser(); return; }

Try / catch

try { copyMedia(items); } catch (ClientZonedDateTimeException e) {} catch (WebApplicationException e) { if (e.getResponse().getStatus() == 413) { handleQuotaExhausted(); } }

Prevention

When it happens

Trigger: POST to the anonymous copy-media batch endpoint where one or more items' media would push total backed-up media bytes over the quota computed by backupManager.getCopyQuota.

Common situations: Backups accumulating past the paid/allowed quota; retrying copy batches after earlier partial failures; large attachment uploads when the user has almost no quota left; stale client assumptions about available space.

Understand the failure class

Background: payload too large / request exceeds maximum size: why libraries cap bytes and how to fix oversize payloads — this error's family across 50 libraries.

Related errors


AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09). Data as JSON: /api/errors/503ba0c386245d1c. Report an issue: GitHub.

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArchiveController.java:773

      throws BackupFailedZkAuthenticationException, BackupWrongCredentialTypeException, BackupPermissionException, BackupInvalidArgumentException {
    if (account.isPresent()) {
      throw new BadRequestException("must not use authenticated connection for anonymous operations");
    }

    final AuthenticatedBackupUser backupUser =
        backupManager.authenticateBackupUser(presentation.presentation, signature.signature, userAgent);
    final BackupManager.CopyQuota copyQuota =
        backupManager.getCopyQuota(backupUser, List.of(copyMediaRequest.toCopyParameters()), maxAttachmentSize);
    final CopyResult copyResult = backupManager.copyToBackup(copyQuota).next()
            .blockOptional()
            .orElseThrow(() -> new IllegalStateException("Non empty copy request must return result"));
    backupMetrics.updateCopyCounter(copyResult, UserAgentTagUtil.getPlatformTag(userAgent));
    return switch (copyResult.outcome()) {
      case SUCCESS -> new CopyMediaResponse(copyResult.cdn());
      case SOURCE_WRONG_LENGTH -> throw new BadRequestException("Invalid length");
      case SOURCE_NOT_FOUND -> throw new ClientErrorException("Source object not found", Response.Status.GONE);
      case OUT_OF_QUOTA ->
          throw new ClientErrorException("Media quota exhausted", Response.Status.REQUEST_ENTITY_TOO_LARGE);
    };
  }

  public record CopyMediaBatchRequest(
      @Schema(description = "A list of media objects to copy from the attachments CDN to the backup CDN")
      @NotNull
      @Size(min = 1, max = 1000)
      List<@Valid CopyMediaRequest> items) {}

  public record CopyMediaBatchResponse(

      @Schema(description = "Detailed outcome information for each copy request in the batch")
      List<Entry> responses) {

    public record Entry(
        @Schema(description = """
            The outcome of the copy attempt.
            A 200 indicates the object was successfully copied.

View on GitHub (pinned to 100ab61c82)