We've been making a lot of scale-forward decisions at Bluesky lately! Last week, there was the release of the BPS site along with Jetstream v2, and just before that, we updated the reference PDS with observability support and larger default blob sizes to support longer videos.

As part of supporting longer videos, we've also published some new Lexicon endpoints that make it possible to upload longer videos in parts, rather than in one long POST. Bluesky's own app is using these new code paths. Other social media dashboards, or other apps that are regularly uploading long videos to Bluesky, might find them really helpful too.

For them, I've put together a TypeScript code sample demonstrating multi-part video upload. You can find it on Tangled:

multipart-video-upload.ts · by alex.bsky.team
Bluesky multipart video upload sample
https://tangled.org/strings/alex.bsky.team/3mtfaddlwrg22

It's about 400 lines of code in a single file. I'll break down what it does and how to use it briefly here.

First, install the needed dependencies, including lex, and use lex to install and build the relevant Lexicons for the project. After that, you can run it as a one-liner with node (thanks for our recent SDK upgrades!), with a path to a video file.

```

npm i @bsky/sdk @atproto/lex @atproto/lex-password-session

lex install app.bsky.video.{startUpload,uploadPart,finishUpload,getUploadStatus,abortUpload,getJobStatus,defs}

lex build --clear --import-ext .ts

ATP_USERNAME=… ATP_PASSWORD=… node --experimental-strip-types post-video.ts clip.mp4

```

By default, it won't actually create a post. Instead, it'll exit and print the completedJobId and the resulting blob. Add --post (with --text) when you actually want a post created.

It exposes a few configuration options, including the number of concurrent uploads:

```

const USAGE = `usage: post-video <file> [options]

Uploads the video and stops, printing the resulting blob. Nothing is posted unless you pass --post.

--post create the post too (off by default)

--text <str> post text, with --post

--mime <type> default video/mp4

--concurrency <n> parallel part uploads (default 3)

--width, --height advisory dimensions; also sets the embed aspect ratio

--resume <jobId> finish an interrupted upload, re-sending only the gaps

env: ATP_USERNAME, ATP_PASSWORD`

```

This also lets you finish an interrupted upload without having to start over. The script defaults to a concurrency of 3, which we've found ideal in most environments for parallelizing video uploads.

It handles splitting the file and validating it for you:

```

export function planParts(sizeBytes: number, partSizeBytes: number): PartPlan[] {

const parts: PartPlan[] = []

for (let start = 0; start < sizeBytes; start += partSizeBytes) {

const end = Math.min(start + partSizeBytes, sizeBytes)

parts.push({ partNumber: parts.length + 1, start, end })

}

return parts

}

```

Uploading individual parts is idempotent, so you can retry freely. On a success response from finishUpload, take the completedJobId and poll app.bsky.video.getJobStatus until it's done.

And you can customize it or port it however you like from there — it's meant to serve as a reference example. In particular, we'd recommend always using timeouts and retries around uploadPart to optimize calling that endpoint. Enjoy, and let us know what you think!