# 2.2.0: Concurrency keys: limit runs per customer, not per job

2026-04-14 · https://galley.themes.dev/changelog/v2-2-concurrency-keys/

> A concurrency limit can now apply per key, so one busy customer can no longer hold up everyone else's runs.

## Concurrency keys

Until now a concurrency limit applied to a whole job: `limit: 20` meant 20 runs at once, across all your customers. When one customer imported 50,000 contacts, everyone else's imports waited behind them.

A limit can now take a key. Runnel keeps a separate count for each key value, so the limit below allows 2 runs at a time per account and any number of accounts at once.

```ts
export const importContacts = runnel.job({
  name: 'import-contacts',
  concurrency: { limit: 2, key: (p) => p.accountId },
  run: async (payload: { accountId: string; fileId: string }, ctx) => { /* … */ },
});
```

Python takes `concurrency=Concurrency(limit=2, key=lambda p: p["account_id"])`, and Go `runnel.WithConcurrencyKey(2, fn)`. Runs waiting for a slot show as `queued` with the reason "Waiting for concurrency key acc_42" in the dashboard.

Keys are free on every plan. A job can have up to 100,000 active keys.

## Fixes

- A run cancelled while its attempt was starting could be marked `running` for up to a minute.
- `runnel dev` now reloads when `runnel.config.ts` changes, not only when job files do.
- Fixed a memory leak in the TypeScript worker when a job logged objects with circular references.
