---
title: MySQL
description: Community MySQL state adapter for Chat SDK built on mysql2. Persistence, distributed locking, caching, lists, and queues without a separate Redis dependency.
tagline: Community MySQL state adapter for Chat SDK, for apps that already use MySQL as their primary datastore and don't want to run a separate Redis cluster.
package: chat-state-mysql
---

# MySQL



## Install

<PackageInstall package="chat chat-state-mysql" />

## Quick start

`createMySqlState()` auto-detects `MYSQL_URL` (or `DATABASE_URL`), so you can call it with no arguments:

```typescript title="lib/bot.ts" lineNumbers
import { Chat } from "chat";
import { createMySqlState } from "chat-state-mysql";

const bot = new Chat({
  userName: "mybot",
  adapters: {
    /* ... */
  },
  state: createMySqlState(),
});
```

To provide a URL explicitly:

```typescript
const state = createMySqlState({
  url: "mysql://root:root@localhost:3306/chat",
});
```

## Configuration

<TypeTable
  type={{
  url: {
    type: "string",
    description:
      "MySQL connection URL. Auto-detected from `MYSQL_URL` or `DATABASE_URL` when omitted.",
  },
  client: {
    type: "mysql.Pool",
    description:
      "Existing `mysql2/promise` Pool instance to reuse instead of creating a new one.",
  },
  keyPrefix: {
    type: "string",
    default: '"chat-sdk"',
    description: "Prefix for all state rows.",
  },
  logger: {
    type: "Logger",
    default: 'ConsoleLogger("info").child("mysql")',
    description: "Logger instance.",
  },
}}
/>

Either `url`, the `MYSQL_URL` / `DATABASE_URL` env var, or `client` is required.

### Environment variables

| Variable       | Required | Description                                                |
| -------------- | -------- | ---------------------------------------------------------- |
| `MYSQL_URL`    | No       | MySQL connection URL, auto-detected when `url` is omitted. |
| `DATABASE_URL` | No       | Alternative to `MYSQL_URL`, also auto-detected.            |

## Using an existing client

Pass an existing `mysql2/promise` pool as `client` to reuse it:

```typescript
import mysql from "mysql2/promise";

const client = mysql.createPool(process.env.MYSQL_URL);
const state = createMySqlState({ client });
```

## Data model

The adapter creates these tables automatically on `connect()`:

```sql
chat_state_subscriptions
chat_state_locks
chat_state_cache
chat_state_lists
chat_state_queues
```

All rows are namespaced by `key_prefix`. Prefixes, thread IDs, and cache keys are stored as text, with SHA-256 hash columns used for MySQL primary keys and indexes, so long platform IDs and multibyte prefixes remain supported.

The schema avoids window functions and generated columns, and is compatible with MySQL 5.7 and newer.

## Locking considerations

The Redis state adapters use atomic `SET NX PX` for lock acquisition. The MySQL adapter uses InnoDB row-level locking through `INSERT ... ON DUPLICATE KEY UPDATE`, replacing a lock only when the stored `expires_at` timestamp has passed. This is safe for typical multi-instance workloads, but Redis remains a better fit for high-contention distributed locking.

## Expired row cleanup

Unlike Redis, MySQL does not automatically delete expired rows. The adapter performs opportunistic cleanup:

* Expired locks are overwritten on the next `acquireLock()` call.
* Expired cache entries are deleted on the next `get()` call for that key.
* Expired queue entries are purged during queue operations.

For high-throughput deployments, run a periodic cleanup job:

```sql
DELETE FROM chat_state_locks WHERE expires_at <= CURRENT_TIMESTAMP(3);
DELETE FROM chat_state_cache WHERE expires_at <= CURRENT_TIMESTAMP(3);
DELETE FROM chat_state_lists WHERE expires_at <= CURRENT_TIMESTAMP(3);
DELETE FROM chat_state_queues WHERE expires_at <= CURRENT_TIMESTAMP(3);
```

## Feature support

<FeatureSupport />
