# pastebin-worker keeps small pastes in KV so they expire, and large ones in R2 where nothing expires them

> pastebin-worker is a Cloudflare Workers pastebin with a browser front end, an HTTP API, a Python command line client, and an agent-facing API summary. The storage split is the design decision worth understanding: KV holds small pastes and expires them natively, R2 holds anything above a threshold constant whose value the documentation never states.

**SharzyL/pastebin-worker** — Pastebin on Cloudflare worker, with friendly CLI usage and rich features

- Repository: https://github.com/SharzyL/pastebin-worker
- Website: https://shz.al
- Stars: 1,062 · Forks: 367
- Language: TypeScript
- License: MIT
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/sharzyl-pastebin-worker

## The store that expires your paste is not the store that holds big ones

One note carries the whole storage design. Small pastes go to KV rather than R2 to keep garbage collection cheap, and KV honours per-key expiration natively, so expired pastes vanish on their own. R2 has no built-in expiration, so cleaning up expired objects would require periodically listing and scanning every object in the bucket, which gets costly in Class A and Class B operations as the bucket grows. In other words, expiry is a property of the small-paste path and not of the service. The split itself is a configuration constant, `R2_THRESHOLD`, and no section of the project documentation gives its default value or its units. So which expiry behaviour you get depends on a number you will have to go read in the deployed configuration.

## One delete command exists and it only reaches KV

The administration section is four lines long. Deleting a paste is `pnpm delete-paste <name-of-paste>`, and that script is `wrangler kv key delete --binding PB`. Listing pastes is `pnpm -s wrangler kv key list --binding PB > kv_list.json`, which writes the whole key list to a JSON file in your working directory. Both commands address the KV binding named PB, and neither has an R2 counterpart. Combined with the note that R2 objects do not expire, that leaves a gap you should plan for: a large paste, meaning one above `R2_THRESHOLD`, has no documented administrative delete path and no automatic expiry, so nothing in the documented workflow retires it.

## The 100 MB cap belongs to the platform, not to the worker

A single request body is capped at 100 MB by Cloudflare, and the platform returns HTTP `413` for anything larger before the worker runs at all. So the limit is not something the worker enforces and not something a larger Worker plan would lift. The escape hatch is the client: the website and the `pb` CLI transparently chunk the upload. That CLI is the surprise in a TypeScript repository, since it is a Python script requiring Python 3.9 and the `requests` package, and it switches to multipart upload above 5 MiB and shows a progress bar. There is also `doc/skill.md`, described as a concise, agent-oriented packaging of the API so a coding agent can upload, fetch, and manage pastes through the service.

## Private mode locks the static pages and its examples disagree

Private mode is one variables block, `[vars.BASIC_AUTH]`, holding username to bcrypt hash pairs, with hashes generated by running `./scripts/bcrypt.js`. Once it is present, every POST request and every access to a static page requires HTTP basic auth, so it is site-wide rather than upload-only. The worked example does not line up with its own configuration. The block declares `user1` and `user2`, while the curl examples authenticate as `admin1` and, on the one that succeeds, use `this-is-passwd-1` in clear text. The successful response is worth reading for what the API hands back:

```console
$ curl -u admin1:this-is-passwd-1 -Fc=@/path/to/file example-pb.com
{
  "url": "https://example-pb.com/YCDX",
  "admin": "https://example-pb.com/YCDX:Sij23HwbMjeZwKznY3K5trG8",
  "isPrivate": false
}
```

Every upload returns two URLs, a public one and a longer `admin` URL that carries the management token.

## The free tier is capped by KV writes, not by storage

The cost section does the arithmetic for you, and the binding constraint is clear. On the free plan KV allows 1 k writes a day, which is 1 k uploads a day, and roughly 100 k fetches a day across KV and Workers, with 1 GB of small-paste storage in KV and 10 GB-month of large-paste storage in R2. On the five dollar Workers plan the included monthly KV allowance works out to about 33 k uploads a day and about 333 k fetches a day, with Workers requests included to about 10 M a month. KV has no paid plan of its own, so the higher limits arrive with the Workers plan rather than separately. Workers Logs are optional and off unless enabled in `wrangler.toml`, at 200 k events a day free with three-day retention, and verbose logging is named as one of the three things that drives cost.

## The package is private, called pb, and sits on a branch called goshujin

package.json names the project `pb` at version 1.0.0 with `private` set to true, and the repository has no GitHub releases, so 1.0.0 in the file is the only version marker anywhere. The default branch is `goshujin`, not main or master, which is what a clone lands on. The same name runs through the deployment: the KV namespace is created as `pnpm wrangler kv namespace create PB`, the R2 bucket takes a name of your choosing, and the binding referenced by the administration commands is PB. The dependency list is entirely devDependencies, from wrangler and vite through vitest, eslint, typescript, prettier, msw, and jsdom, so the deployed worker carries no runtime npm package at all.

## Frontend and worker build separately and the dev build points elsewhere

The development section states that the frontend and the worker are built separately, which shapes every command. `pnpm dev:frontend` starts a Vite development server for the front end. To work on the worker you first have to produce a development build of the frontend with `pnpm build:frontend:dev`, and only then run `pnpm dev`, which starts a local worker on port 8787 with `DEPLOY_URL` set to that address and an index page title of `Pastebin Worker (dev)`. The documented difference between the two frontend builds is which API endpoint they bake in: the development one points at your deployment URL, the normal one at `http://localhost:8787`. Checks are `pnpm test`, `pnpm coverage`, then `pnpm fmt`, `pnpm lint`, and `pnpm typecheck` before committing. The repository also carries a `toml` parser as a dev dependency and a generated `worker-configuration.d.ts`.

## Conclusion

pastebin-worker is a good fit for a small private or community instance, because the cost section works out exactly what the free tier and the five dollar plan hold, and because private mode is a single variables block. It is a poor fit as a general public pastebin, because KV writes cap uploads at a thousand a day on the free tier, and it is a poor fit if you need expiry on large pastes, because R2 has none and the only delete command in the project reaches KV. Before deploying, find the value of `R2_THRESHOLD` in `wrangler.toml`, decide who can delete R2-resident pastes, and read the auth section twice, since its examples name a different user than its own configuration block does.

## FAQ

### What is pastebin-worker and what can it do?

It is a pastebin running on Cloudflare Workers, live at shz.al. Pastes can be shared with a name as short as four characters or a custom URL, syntax highlighting comes from highlight.js, there is client-side encryption, Markdown pastes are served as rendered HTML, it doubles as a URL shortener, and Content-Type and Content-Disposition handling is adjustable.

### How do I deploy pastebin-worker to my own domain?

Install node and pnpm, clone the repository, create a KV namespace and an R2 bucket with `pnpm wrangler kv namespace create PB` and `pnpm wrangler r2 bucket create <name>`, put those values into `wrangler.toml`, then run `pnpm install`, `pnpm wrangler login`, `pnpm build:frontend`, and `pnpm deploy`. The domain has to be hosted on Cloudflare.

### Why do some pastes on pastebin-worker never expire?

Small pastes are stored in Workers KV, which honors per-key expiration natively, so they vanish on their own. Pastes above the `R2_THRESHOLD` constant are stored in R2, which has no built-in expiration, and cleaning them up would mean periodically listing and scanning every object in the bucket at additional Class A and Class B cost. The value of `R2_THRESHOLD` is not given in the documentation.

### How do I make a pastebin-worker deployment private?

Add a `[vars.BASIC_AUTH]` entry to `wrangler.toml` mapping usernames to bcrypt hashes, which you can generate with `./scripts/bcrypt.js`. After that every POST request and every access to a static page requires HTTP basic auth, so it covers the whole site rather than uploads alone. The documented example block declares user1 and user2, while the curl examples authenticate as admin1.

### What are the size limits for uploading to pastebin-worker?

A single request body is capped at 100 MB by Cloudflare, and the platform returns HTTP `413` for larger bodies before the worker runs. For larger files the website and the `pb` CLI chunk the upload transparently. The `pb` CLI is a Python script needing Python 3.9 and `requests`, and it switches to multipart upload above 5 MiB.

## Sources

- [Issues](https://github.com/SharzyL/pastebin-worker/issues)
- [License: MIT](https://github.com/SharzyL/pastebin-worker/blob/goshujin/LICENSE)
- [Project website](https://shz.al)
- [README](https://github.com/SharzyL/pastebin-worker/blob/goshujin/README.md)
- [SharzyL/pastebin-worker on GitHub](https://github.com/SharzyL/pastebin-worker)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/sharzyl-pastebin-worker
