From 058395520558215d0bbbea64bbad997623491e54 Mon Sep 17 00:00:00 2001 From: Dikran Samarjian Date: Wed, 3 Jun 2026 12:47:34 -0700 Subject: [PATCH 1/4] new redis best practices --- docs/guides/best-practices/redis.md | 180 ++++++++++++++++++ sidebars.ts | 1 + .../guides/best-practices/redis.md | 179 +++++++++++++++++ .../guides/best-practices/redis.md | 179 +++++++++++++++++ versioned_sidebars/version-0.12-sidebars.json | 1 + versioned_sidebars/version-0.13-sidebars.json | 1 + 6 files changed, 541 insertions(+) create mode 100644 docs/guides/best-practices/redis.md create mode 100644 versioned_docs/version-0.12/guides/best-practices/redis.md create mode 100644 versioned_docs/version-0.13/guides/best-practices/redis.md diff --git a/docs/guides/best-practices/redis.md b/docs/guides/best-practices/redis.md new file mode 100644 index 00000000..44d57f44 --- /dev/null +++ b/docs/guides/best-practices/redis.md @@ -0,0 +1,180 @@ +# Using Redis + +Redis is the main way to store app data in Devvit. It works well for leaderboards, player state, moderation queues, cached API responses, and other data your app needs across requests. + +This guide provides practical tips to help you build Redis-backed features that are reliable, easy to maintain, and ready for Reddit communities. For the full command list and API details, see the [Redis capability reference](../../capabilities/server/redis.mdx). + +## Data storage + +Each installation of your app has its own Redis storage. An app installed on one subreddit does not share Redis data with the same app installed on another subreddit. + +This is the right model for most community games, mod tools, settings, leaderboards, and user state. Build with the assumption that each subreddit installation is its own environment. + +If your app needs cross-community data, like a global leaderboard or analytics rollup, plan for that directly. Use a shared service through [HTTP Fetch](../../capabilities/server/http-fetch.mdx) instead of assuming every installation can read the same keys. + +## Key design + +Devvit Redis does not support listing every key in an installation. If your app loses track of a key name, it cannot discover that key later with a global scan. + +Avoid scattering related data across too many independent keys: + +```ts +await redis.set(`profile:${userId}`, JSON.stringify(profile)); +await redis.set(`profile:${otherUserId}`, JSON.stringify(otherProfile)); +``` + +Use stable collection keys when you need to read or update related records: + +```ts +await redis.hSet('app:user_profiles', { + [userId]: JSON.stringify(profile), + [otherUserId]: JSON.stringify(otherProfile), +}); + +const profiles = await redis.hScan('app:user_profiles', 0); +``` + +This gives your app a known place to look and lets you iterate within the collection with `hScan` or `hKeys`. + +## Data structures + +Redis in Devvit supports strings, hashes, sorted sets, numbers, bitfields, and transactions. It does not support every Redis data type, including plain sets and lists. + +Use these patterns when you need set-like or queue-like behavior. + +| Use case | Recommended pattern | +| --- | --- | +| Unique unordered collection | Use a sorted set and give every member the same score, like `0`. | +| Leaderboard | Use a sorted set with the score as the ranking value. | +| FIFO queue | Use a sorted set with a timestamp score, read the oldest entries with `zRange`, then remove processed entries with `zRem`. | +| Dense flags or compact counters | Use `bitfield` when you need bitmap-style storage for many small values. | +| Temporary data | Use `expire` for short-lived keys, or remove old sorted set entries by score. | + +For command-specific behavior and limits, see the [supported Redis commands](../../capabilities/server/redis.mdx#supported-redis-commands). + +## Large values + +:::note +Before you choose an external storage provider, make sure it follows the [HTTP Fetch policy](../../capabilities/server/http-fetch-policy.md). +::: + +Redis is best for small, frequently accessed app state. Use a dedicated blob or object storage service for large objects, generated files, long archives, or data your app reads as a whole instead of querying field by field. Devvit apps can enable the [Blob Plugin](../../api/public-api/type-aliases/Configuration.md#blob), and you can connect to external storage services with [HTTP Fetch](../../capabilities/server/http-fetch.mdx). Store the object ID and lightweight metadata in Redis so your app can still look things up quickly. + +Each Redis installation has a 500 MB storage limit and a 5 MB request size limit. If your app stores large JSON objects, long text values, or historical records that still belong in Redis, consider using `redisCompressed`. + +```ts +import { redisCompressed } from '@devvit/redis'; + +await redisCompressed.set('cache:heavy_payload', JSON.stringify(payload)); +``` + +The compressed client compresses supported writes when compression saves space, and decompresses those values on supported reads. + +**Tip**: Do not mix `redis` and `redisCompressed` on the same keys. Data written by `redisCompressed` should be read with `redisCompressed`. To migrate existing data, read the old values with the standard client and write them back with the compressed client. For a complete migration example, see [Compression](../../capabilities/server/redis.mdx#compression-experimental). + +## Shared state + +Devvit app requests are stateless and can run concurrently. Popular posts, games, comment triggers, and moderator actions can all cause multiple requests to update the same data at the same time. + +Use Redis transactions when correctness depends on reading a value, checking it, and writing a new value based on the result. + +Good candidates for transactions include: + +- Spending points, coins, inventory, or other balances. +- Claiming a limited reward where only one user should win. +- Enforcing one vote, entry, redemption, or action per user. +- Updating multiple related keys that must stay in sync. + +You usually do not need a transaction for single-command atomic operations, like `incrBy`, `hIncrBy`, or `zIncrBy`, unless the update depends on a separate read or validation step. + +```ts +async function deductBalance(userId: string, cost: number): Promise { + const key = `user:${userId}:balance`; + const txn = await redis.watch(key); + + const currentRaw = await txn.get(key); + const current = currentRaw ? Number.parseInt(currentRaw, 10) : 0; + + if (current < cost) { + await txn.unwatch(); + return false; + } + + await txn.multi(); + await txn.set(key, String(current - cost)); + + const result = await txn.exec(); + return result !== null; +} +``` + +Transactions are limited to 20 concurrent transaction blocks per installation, and transaction execution has a 5-second timeout. Always call `unwatch()` or `discard()` when your code exits early before `exec()`. + +For more details, see [Transactions](../../capabilities/server/redis.mdx#transactions). + +## Quota failures + +When an installation reaches the Redis storage limit, write operations can fail. Treat Redis writes as operations that may need a user-facing fallback. + +A good failure path should: + +- Prevent the failed write from crashing the request. +- Keep read-only behavior working where possible. +- Tell the user or moderator what happened and which actions are temporarily unavailable. + +```ts +type WriteResult = + | { ok: true } + | { ok: false; reason: 'storage-quota' } + | { ok: false; reason: 'unknown'; message: string }; + +async function savePlayerProfile(userId: string, profile: PlayerProfile): Promise { + try { + await redis.hSet('app:player_profiles', { + [userId]: JSON.stringify(profile), + }); + + return { ok: true }; + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown Redis error'; + const normalized = message.toLowerCase(); + + if (normalized.includes('quota') || normalized.includes('storage')) { + return { ok: false, reason: 'storage-quota' }; + } + + return { ok: false, reason: 'unknown', message }; + } +} +``` + +When storage is full, avoid deleting arbitrary data at write time. Make cleanup predictable: use TTLs for temporary keys, cap sorted sets to the number of entries you actually display, and run scheduled cleanup jobs for old records. + +## Scheduled maintenance + +Large migrations, leaderboard maintenance, and cleanup jobs can exceed request time limits if they process everything in one request. Use the scheduler to process a bounded batch, save the cursor or progress marker, and schedule the next batch. + +This pattern works well for: + +- Migrating uncompressed data to `redisCompressed`. +- Rebuilding derived indexes. +- Cleaning up old sorted set entries. +- Backfilling new fields in large hashes. + +The Redis reference includes a full scheduled migration example. The Scheduler reference covers recurring and one-off jobs, including platform limits for `runJob()`. + +- [Redis migration example](../../capabilities/server/redis.mdx#migration-example) +- [Scheduler](../../capabilities/server/scheduler.mdx) + +## Platform limits + +Design around these limits early so your app stays responsive as more people use it. + +| Platform behavior | Limit | Best practice | +| --- | --- | --- | +| Command throughput | 40,000 commands per second per installation | Avoid one Redis call per item in hot loops. Use batch commands such as `mGet`, `mSet`, and hash writes where they fit. | +| Pipelining | Not supported | Reduce round trips by grouping related fields under hashes or compact JSON values. | +| Storage | 500 MB per installation | Use `redisCompressed` for large values, expire temporary data, and cap unbounded histories. | +| Request size | 5 MB | Store large datasets as multiple records and process them in batches. | +| Transactions | 20 concurrent transaction blocks; 5-second execution timeout | Keep transaction logic small and release transactions with `exec`, `discard`, or `unwatch`. | + diff --git a/sidebars.ts b/sidebars.ts index a29fdebd..c6802640 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -283,6 +283,7 @@ const sidebars: SidebarsConfig = { label: "Best Practices", items: [ "guides/best-practices/community_games", + "guides/best-practices/redis", "guides/best-practices/mod_resources", "capabilities/server/text_fallback", ], diff --git a/versioned_docs/version-0.12/guides/best-practices/redis.md b/versioned_docs/version-0.12/guides/best-practices/redis.md new file mode 100644 index 00000000..5faadc2c --- /dev/null +++ b/versioned_docs/version-0.12/guides/best-practices/redis.md @@ -0,0 +1,179 @@ +# Using Redis + +Redis is the main way to store app data in Devvit. It works well for leaderboards, player state, moderation queues, cached API responses, and other data your app needs across requests. + +This guide provides practical tips to help you build Redis-backed features that are reliable, easy to maintain, and ready for Reddit communities. For the full command list and API details, see the [Redis capability reference](../../capabilities/server/redis.mdx). + +## Data storage + +Each installation of your app has its own Redis storage. An app installed on one subreddit does not share Redis data with the same app installed on another subreddit. + +This is the right model for most community games, mod tools, settings, leaderboards, and user state. Build with the assumption that each subreddit installation is its own environment. + +If your app needs cross-community data, like a global leaderboard or analytics rollup, plan for that directly. Use a shared service through [HTTP Fetch](../../capabilities/server/http-fetch.mdx) instead of assuming every installation can read the same keys. + +## Key design + +Devvit Redis does not support listing every key in an installation. If your app loses track of a key name, it cannot discover that key later with a global scan. + +Avoid scattering related data across too many independent keys: + +```ts +await redis.set(`profile:${userId}`, JSON.stringify(profile)); +await redis.set(`profile:${otherUserId}`, JSON.stringify(otherProfile)); +``` + +Use stable collection keys when you need to read or update related records: + +```ts +await redis.hSet('app:user_profiles', { + [userId]: JSON.stringify(profile), + [otherUserId]: JSON.stringify(otherProfile), +}); + +const profiles = await redis.hScan('app:user_profiles', 0); +``` + +This gives your app a known place to look and lets you iterate within the collection with `hScan` or `hKeys`. + +## Data structures + +Redis in Devvit supports strings, hashes, sorted sets, numbers, bitfields, and transactions. It does not support every Redis data type, including plain sets and lists. + +Use these patterns when you need set-like or queue-like behavior. + +| Use case | Recommended pattern | +| --- | --- | +| Unique unordered collection | Use a sorted set and give every member the same score, like `0`. | +| Leaderboard | Use a sorted set with the score as the ranking value. | +| FIFO queue | Use a sorted set with a timestamp score, read the oldest entries with `zRange`, then remove processed entries with `zRem`. | +| Dense flags or compact counters | Use `bitfield` when you need bitmap-style storage for many small values. | +| Temporary data | Use `expire` for short-lived keys, or remove old sorted set entries by score. | + +For command-specific behavior and limits, see the [supported Redis commands](../../capabilities/server/redis.mdx#supported-redis-commands). + +## Large values + +:::note +Before you choose an external storage provider, make sure it follows the [HTTP Fetch policy](../../capabilities/server/http-fetch-policy.md). +::: + +Redis is best for small, frequently accessed app state. Use a dedicated blob or object storage service for large objects, generated files, long archives, or data your app reads as a whole instead of querying field by field. Devvit apps can enable the [Blob Plugin](../../api/public-api/type-aliases/Configuration.md#blob), and you can connect to external storage services with [HTTP Fetch](../../capabilities/server/http-fetch.mdx). Store the object ID and lightweight metadata in Redis so your app can still look things up quickly. + +Each Redis installation has a 500 MB storage limit and a 5 MB request size limit. If your app stores large JSON objects, long text values, or historical records that still belong in Redis, consider using `redisCompressed`. + +```ts +import { redisCompressed } from '@devvit/redis'; + +await redisCompressed.set('cache:heavy_payload', JSON.stringify(payload)); +``` + +The compressed client compresses supported writes when compression saves space, and decompresses those values on supported reads. + +**Tip**: Do not mix `redis` and `redisCompressed` on the same keys. Data written by `redisCompressed` should be read with `redisCompressed`. To migrate existing data, read the old values with the standard client and write them back with the compressed client. For a complete migration example, see [Compression](../../capabilities/server/redis.mdx#compression-experimental). + +## Shared state + +Devvit app requests are stateless and can run concurrently. Popular posts, games, comment triggers, and moderator actions can all cause multiple requests to update the same data at the same time. + +Use Redis transactions when correctness depends on reading a value, checking it, and writing a new value based on the result. + +Good candidates for transactions include: + +- Spending points, coins, inventory, or other balances. +- Claiming a limited reward where only one user should win. +- Enforcing one vote, entry, redemption, or action per user. +- Updating multiple related keys that must stay in sync. + +You usually do not need a transaction for single-command atomic operations, like `incrBy`, `hIncrBy`, or `zIncrBy`, unless the update depends on a separate read or validation step. + +```ts +async function deductBalance(userId: string, cost: number): Promise { + const key = `user:${userId}:balance`; + const txn = await redis.watch(key); + + const currentRaw = await txn.get(key); + const current = currentRaw ? Number.parseInt(currentRaw, 10) : 0; + + if (current < cost) { + await txn.unwatch(); + return false; + } + + await txn.multi(); + await txn.set(key, String(current - cost)); + + const result = await txn.exec(); + return result !== null; +} +``` + +Transactions are limited to 20 concurrent transaction blocks per installation, and transaction execution has a 5-second timeout. Always call `unwatch()` or `discard()` when your code exits early before `exec()`. + +For more details, see [Transactions](../../capabilities/server/redis.mdx#transactions). + +## Quota failures + +When an installation reaches the Redis storage limit, write operations can fail. Treat Redis writes as operations that may need a user-facing fallback. + +A good failure path should: + +- Prevent the failed write from crashing the request. +- Keep read-only behavior working where possible. +- Tell the user or moderator what happened and which actions are temporarily unavailable. + +```ts +type WriteResult = + | { ok: true } + | { ok: false; reason: 'storage-quota' } + | { ok: false; reason: 'unknown'; message: string }; + +async function savePlayerProfile(userId: string, profile: PlayerProfile): Promise { + try { + await redis.hSet('app:player_profiles', { + [userId]: JSON.stringify(profile), + }); + + return { ok: true }; + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown Redis error'; + const normalized = message.toLowerCase(); + + if (normalized.includes('quota') || normalized.includes('storage')) { + return { ok: false, reason: 'storage-quota' }; + } + + return { ok: false, reason: 'unknown', message }; + } +} +``` + +When storage is full, avoid deleting arbitrary data at write time. Make cleanup predictable: use TTLs for temporary keys, cap sorted sets to the number of entries you actually display, and run scheduled cleanup jobs for old records. + +## Scheduled maintenance + +Large migrations, leaderboard maintenance, and cleanup jobs can exceed request time limits if they process everything in one request. Use the scheduler to process a bounded batch, save the cursor or progress marker, and schedule the next batch. + +This pattern works well for: + +- Migrating uncompressed data to `redisCompressed`. +- Rebuilding derived indexes. +- Cleaning up old sorted set entries. +- Backfilling new fields in large hashes. + +The Redis reference includes a full scheduled migration example. The Scheduler reference covers recurring and one-off jobs, including platform limits for `runJob()`. + +- [Redis migration example](../../capabilities/server/redis.mdx#migration-example) +- [Scheduler](../../capabilities/server/scheduler.mdx) + +## Platform limits + +Design around these limits early so your app stays responsive as more people use it. + +| Platform behavior | Limit | Best practice | +| --- | --- | --- | +| Command throughput | 40,000 commands per second per installation | Avoid one Redis call per item in hot loops. Use batch commands such as `mGet`, `mSet`, and hash writes where they fit. | +| Pipelining | Not supported | Reduce round trips by grouping related fields under hashes or compact JSON values. | +| Storage | 500 MB per installation | Use `redisCompressed` for large values, expire temporary data, and cap unbounded histories. | +| Request size | 5 MB | Store large datasets as multiple records and process them in batches. | +| Transactions | 20 concurrent transaction blocks; 5-second execution timeout | Keep transaction logic small and release transactions with `exec`, `discard`, or `unwatch`. | diff --git a/versioned_docs/version-0.13/guides/best-practices/redis.md b/versioned_docs/version-0.13/guides/best-practices/redis.md new file mode 100644 index 00000000..5faadc2c --- /dev/null +++ b/versioned_docs/version-0.13/guides/best-practices/redis.md @@ -0,0 +1,179 @@ +# Using Redis + +Redis is the main way to store app data in Devvit. It works well for leaderboards, player state, moderation queues, cached API responses, and other data your app needs across requests. + +This guide provides practical tips to help you build Redis-backed features that are reliable, easy to maintain, and ready for Reddit communities. For the full command list and API details, see the [Redis capability reference](../../capabilities/server/redis.mdx). + +## Data storage + +Each installation of your app has its own Redis storage. An app installed on one subreddit does not share Redis data with the same app installed on another subreddit. + +This is the right model for most community games, mod tools, settings, leaderboards, and user state. Build with the assumption that each subreddit installation is its own environment. + +If your app needs cross-community data, like a global leaderboard or analytics rollup, plan for that directly. Use a shared service through [HTTP Fetch](../../capabilities/server/http-fetch.mdx) instead of assuming every installation can read the same keys. + +## Key design + +Devvit Redis does not support listing every key in an installation. If your app loses track of a key name, it cannot discover that key later with a global scan. + +Avoid scattering related data across too many independent keys: + +```ts +await redis.set(`profile:${userId}`, JSON.stringify(profile)); +await redis.set(`profile:${otherUserId}`, JSON.stringify(otherProfile)); +``` + +Use stable collection keys when you need to read or update related records: + +```ts +await redis.hSet('app:user_profiles', { + [userId]: JSON.stringify(profile), + [otherUserId]: JSON.stringify(otherProfile), +}); + +const profiles = await redis.hScan('app:user_profiles', 0); +``` + +This gives your app a known place to look and lets you iterate within the collection with `hScan` or `hKeys`. + +## Data structures + +Redis in Devvit supports strings, hashes, sorted sets, numbers, bitfields, and transactions. It does not support every Redis data type, including plain sets and lists. + +Use these patterns when you need set-like or queue-like behavior. + +| Use case | Recommended pattern | +| --- | --- | +| Unique unordered collection | Use a sorted set and give every member the same score, like `0`. | +| Leaderboard | Use a sorted set with the score as the ranking value. | +| FIFO queue | Use a sorted set with a timestamp score, read the oldest entries with `zRange`, then remove processed entries with `zRem`. | +| Dense flags or compact counters | Use `bitfield` when you need bitmap-style storage for many small values. | +| Temporary data | Use `expire` for short-lived keys, or remove old sorted set entries by score. | + +For command-specific behavior and limits, see the [supported Redis commands](../../capabilities/server/redis.mdx#supported-redis-commands). + +## Large values + +:::note +Before you choose an external storage provider, make sure it follows the [HTTP Fetch policy](../../capabilities/server/http-fetch-policy.md). +::: + +Redis is best for small, frequently accessed app state. Use a dedicated blob or object storage service for large objects, generated files, long archives, or data your app reads as a whole instead of querying field by field. Devvit apps can enable the [Blob Plugin](../../api/public-api/type-aliases/Configuration.md#blob), and you can connect to external storage services with [HTTP Fetch](../../capabilities/server/http-fetch.mdx). Store the object ID and lightweight metadata in Redis so your app can still look things up quickly. + +Each Redis installation has a 500 MB storage limit and a 5 MB request size limit. If your app stores large JSON objects, long text values, or historical records that still belong in Redis, consider using `redisCompressed`. + +```ts +import { redisCompressed } from '@devvit/redis'; + +await redisCompressed.set('cache:heavy_payload', JSON.stringify(payload)); +``` + +The compressed client compresses supported writes when compression saves space, and decompresses those values on supported reads. + +**Tip**: Do not mix `redis` and `redisCompressed` on the same keys. Data written by `redisCompressed` should be read with `redisCompressed`. To migrate existing data, read the old values with the standard client and write them back with the compressed client. For a complete migration example, see [Compression](../../capabilities/server/redis.mdx#compression-experimental). + +## Shared state + +Devvit app requests are stateless and can run concurrently. Popular posts, games, comment triggers, and moderator actions can all cause multiple requests to update the same data at the same time. + +Use Redis transactions when correctness depends on reading a value, checking it, and writing a new value based on the result. + +Good candidates for transactions include: + +- Spending points, coins, inventory, or other balances. +- Claiming a limited reward where only one user should win. +- Enforcing one vote, entry, redemption, or action per user. +- Updating multiple related keys that must stay in sync. + +You usually do not need a transaction for single-command atomic operations, like `incrBy`, `hIncrBy`, or `zIncrBy`, unless the update depends on a separate read or validation step. + +```ts +async function deductBalance(userId: string, cost: number): Promise { + const key = `user:${userId}:balance`; + const txn = await redis.watch(key); + + const currentRaw = await txn.get(key); + const current = currentRaw ? Number.parseInt(currentRaw, 10) : 0; + + if (current < cost) { + await txn.unwatch(); + return false; + } + + await txn.multi(); + await txn.set(key, String(current - cost)); + + const result = await txn.exec(); + return result !== null; +} +``` + +Transactions are limited to 20 concurrent transaction blocks per installation, and transaction execution has a 5-second timeout. Always call `unwatch()` or `discard()` when your code exits early before `exec()`. + +For more details, see [Transactions](../../capabilities/server/redis.mdx#transactions). + +## Quota failures + +When an installation reaches the Redis storage limit, write operations can fail. Treat Redis writes as operations that may need a user-facing fallback. + +A good failure path should: + +- Prevent the failed write from crashing the request. +- Keep read-only behavior working where possible. +- Tell the user or moderator what happened and which actions are temporarily unavailable. + +```ts +type WriteResult = + | { ok: true } + | { ok: false; reason: 'storage-quota' } + | { ok: false; reason: 'unknown'; message: string }; + +async function savePlayerProfile(userId: string, profile: PlayerProfile): Promise { + try { + await redis.hSet('app:player_profiles', { + [userId]: JSON.stringify(profile), + }); + + return { ok: true }; + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown Redis error'; + const normalized = message.toLowerCase(); + + if (normalized.includes('quota') || normalized.includes('storage')) { + return { ok: false, reason: 'storage-quota' }; + } + + return { ok: false, reason: 'unknown', message }; + } +} +``` + +When storage is full, avoid deleting arbitrary data at write time. Make cleanup predictable: use TTLs for temporary keys, cap sorted sets to the number of entries you actually display, and run scheduled cleanup jobs for old records. + +## Scheduled maintenance + +Large migrations, leaderboard maintenance, and cleanup jobs can exceed request time limits if they process everything in one request. Use the scheduler to process a bounded batch, save the cursor or progress marker, and schedule the next batch. + +This pattern works well for: + +- Migrating uncompressed data to `redisCompressed`. +- Rebuilding derived indexes. +- Cleaning up old sorted set entries. +- Backfilling new fields in large hashes. + +The Redis reference includes a full scheduled migration example. The Scheduler reference covers recurring and one-off jobs, including platform limits for `runJob()`. + +- [Redis migration example](../../capabilities/server/redis.mdx#migration-example) +- [Scheduler](../../capabilities/server/scheduler.mdx) + +## Platform limits + +Design around these limits early so your app stays responsive as more people use it. + +| Platform behavior | Limit | Best practice | +| --- | --- | --- | +| Command throughput | 40,000 commands per second per installation | Avoid one Redis call per item in hot loops. Use batch commands such as `mGet`, `mSet`, and hash writes where they fit. | +| Pipelining | Not supported | Reduce round trips by grouping related fields under hashes or compact JSON values. | +| Storage | 500 MB per installation | Use `redisCompressed` for large values, expire temporary data, and cap unbounded histories. | +| Request size | 5 MB | Store large datasets as multiple records and process them in batches. | +| Transactions | 20 concurrent transaction blocks; 5-second execution timeout | Keep transaction logic small and release transactions with `exec`, `discard`, or `unwatch`. | diff --git a/versioned_sidebars/version-0.12-sidebars.json b/versioned_sidebars/version-0.12-sidebars.json index 1fa26fab..f3198e6d 100644 --- a/versioned_sidebars/version-0.12-sidebars.json +++ b/versioned_sidebars/version-0.12-sidebars.json @@ -256,6 +256,7 @@ "label": "Best Practices", "items": [ "guides/best-practices/community_games", + "guides/best-practices/redis", "guides/best-practices/mod_resources", "capabilities/server/text_fallback" ] diff --git a/versioned_sidebars/version-0.13-sidebars.json b/versioned_sidebars/version-0.13-sidebars.json index a5a499cf..eaa18c9b 100644 --- a/versioned_sidebars/version-0.13-sidebars.json +++ b/versioned_sidebars/version-0.13-sidebars.json @@ -276,6 +276,7 @@ "label": "Best Practices", "items": [ "guides/best-practices/community_games", + "guides/best-practices/redis", "guides/best-practices/mod_resources", "capabilities/server/text_fallback" ] From dc171810fe6e87ba2b1ae70f09c9fcd74aa969a5 Mon Sep 17 00:00:00 2001 From: Dikran Samarjian Date: Wed, 3 Jun 2026 13:01:12 -0700 Subject: [PATCH 2/4] added new action for previews --- .github/workflows/preview.yml | 124 ++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 .github/workflows/preview.yml diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml new file mode 100644 index 00000000..d5cdcef4 --- /dev/null +++ b/.github/workflows/preview.yml @@ -0,0 +1,124 @@ +name: Preview Documentation + +on: + issue_comment: + types: + - created + +permissions: + contents: read + issues: write + pull-requests: read + +concurrency: + group: preview-pr-${{ github.event.issue.number }} + cancel-in-progress: true + +jobs: + preview: + name: Build and serve preview + if: >- + github.event.issue.pull_request && + ( + github.event.comment.body == 'preview' || + github.event.comment.body == '/preview' + ) && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) + runs-on: ubuntu-latest + timeout-minutes: 75 + + steps: + - name: Get pull request + id: pr + uses: actions/github-script@v7 + with: + script: | + const { owner, repo } = context.repo; + const pull_number = context.payload.issue.number; + const { data: pull } = await github.rest.pulls.get({ + owner, + repo, + pull_number, + }); + + core.setOutput('head_repo', pull.head.repo.full_name); + core.setOutput('head_ref', pull.head.ref); + core.setOutput('head_sha', pull.head.sha); + + - name: React to preview request + uses: actions/github-script@v7 + with: + script: | + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'rocket', + }); + + - name: Checkout pull request + uses: actions/checkout@v4 + with: + repository: ${{ steps.pr.outputs.head_repo }} + ref: ${{ steps.pr.outputs.head_sha }} + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "yarn" + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Build documentation + run: yarn build + + - name: Install cloudflared + run: | + curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb + sudo dpkg -i cloudflared.deb + + - name: Start Docusaurus server + run: | + nohup yarn serve --host 0.0.0.0 --port 3000 > docusaurus-preview.log 2>&1 & + + - name: Start preview tunnel + id: tunnel + run: | + nohup cloudflared tunnel --url http://localhost:3000 --no-autoupdate > cloudflared-preview.log 2>&1 & + + for attempt in {1..30}; do + preview_url="$(grep -o 'https://[^ ]*trycloudflare.com' cloudflared-preview.log | head -n 1 || true)" + if [ -n "$preview_url" ]; then + echo "url=$preview_url" >> "$GITHUB_OUTPUT" + exit 0 + fi + + sleep 2 + done + + echo "Failed to start preview tunnel." + cat cloudflared-preview.log + exit 1 + + - name: Comment preview URL + uses: actions/github-script@v7 + env: + PREVIEW_URL: ${{ steps.tunnel.outputs.url }} + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: [ + `Preview: ${process.env.PREVIEW_URL}`, + '', + 'This preview will stay live for 1 hour. Comment `preview` again to replace it with a fresh build.', + ].join('\n'), + }); + + - name: Keep preview live for one hour + run: sleep 3600 From b9d9a970113370984885b0da8ba92f690953323d Mon Sep 17 00:00:00 2001 From: Dikran Samarjian Date: Wed, 3 Jun 2026 13:07:57 -0700 Subject: [PATCH 3/4] Revert "added new action for previews" This reverts commit dc171810fe6e87ba2b1ae70f09c9fcd74aa969a5. --- .github/workflows/preview.yml | 124 ---------------------------------- 1 file changed, 124 deletions(-) delete mode 100644 .github/workflows/preview.yml diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml deleted file mode 100644 index d5cdcef4..00000000 --- a/.github/workflows/preview.yml +++ /dev/null @@ -1,124 +0,0 @@ -name: Preview Documentation - -on: - issue_comment: - types: - - created - -permissions: - contents: read - issues: write - pull-requests: read - -concurrency: - group: preview-pr-${{ github.event.issue.number }} - cancel-in-progress: true - -jobs: - preview: - name: Build and serve preview - if: >- - github.event.issue.pull_request && - ( - github.event.comment.body == 'preview' || - github.event.comment.body == '/preview' - ) && - contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) - runs-on: ubuntu-latest - timeout-minutes: 75 - - steps: - - name: Get pull request - id: pr - uses: actions/github-script@v7 - with: - script: | - const { owner, repo } = context.repo; - const pull_number = context.payload.issue.number; - const { data: pull } = await github.rest.pulls.get({ - owner, - repo, - pull_number, - }); - - core.setOutput('head_repo', pull.head.repo.full_name); - core.setOutput('head_ref', pull.head.ref); - core.setOutput('head_sha', pull.head.sha); - - - name: React to preview request - uses: actions/github-script@v7 - with: - script: | - await github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: context.payload.comment.id, - content: 'rocket', - }); - - - name: Checkout pull request - uses: actions/checkout@v4 - with: - repository: ${{ steps.pr.outputs.head_repo }} - ref: ${{ steps.pr.outputs.head_sha }} - persist-credentials: false - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "yarn" - - - name: Install dependencies - run: yarn install --frozen-lockfile - - - name: Build documentation - run: yarn build - - - name: Install cloudflared - run: | - curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb - sudo dpkg -i cloudflared.deb - - - name: Start Docusaurus server - run: | - nohup yarn serve --host 0.0.0.0 --port 3000 > docusaurus-preview.log 2>&1 & - - - name: Start preview tunnel - id: tunnel - run: | - nohup cloudflared tunnel --url http://localhost:3000 --no-autoupdate > cloudflared-preview.log 2>&1 & - - for attempt in {1..30}; do - preview_url="$(grep -o 'https://[^ ]*trycloudflare.com' cloudflared-preview.log | head -n 1 || true)" - if [ -n "$preview_url" ]; then - echo "url=$preview_url" >> "$GITHUB_OUTPUT" - exit 0 - fi - - sleep 2 - done - - echo "Failed to start preview tunnel." - cat cloudflared-preview.log - exit 1 - - - name: Comment preview URL - uses: actions/github-script@v7 - env: - PREVIEW_URL: ${{ steps.tunnel.outputs.url }} - with: - script: | - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.issue.number, - body: [ - `Preview: ${process.env.PREVIEW_URL}`, - '', - 'This preview will stay live for 1 hour. Comment `preview` again to replace it with a fresh build.', - ].join('\n'), - }); - - - name: Keep preview live for one hour - run: sleep 3600 From 69f35f2254617e4a1347686235a560a78d94535b Mon Sep 17 00:00:00 2001 From: Dikran Samarjian Date: Wed, 3 Jun 2026 13:37:12 -0700 Subject: [PATCH 4/4] reference deletion hygiene --- docs/guides/best-practices/redis.md | 2 ++ versioned_docs/version-0.12/guides/best-practices/redis.md | 2 ++ versioned_docs/version-0.13/guides/best-practices/redis.md | 2 ++ 3 files changed, 6 insertions(+) diff --git a/docs/guides/best-practices/redis.md b/docs/guides/best-practices/redis.md index 44d57f44..cdfadf6b 100644 --- a/docs/guides/best-practices/redis.md +++ b/docs/guides/best-practices/redis.md @@ -10,6 +10,8 @@ Each installation of your app has its own Redis storage. An app installed on one This is the right model for most community games, mod tools, settings, leaderboards, and user state. Build with the assumption that each subreddit installation is its own environment. +If your app stores user content from Reddit, make sure you remove it from your app when that content is deleted from Reddit. Devvit provides post and comment delete events through triggers to help with this. See [Enable and respect user deletions](../../devvit_rules.md#enable-and-respect-user-deletions) in the Devvit Rules. + If your app needs cross-community data, like a global leaderboard or analytics rollup, plan for that directly. Use a shared service through [HTTP Fetch](../../capabilities/server/http-fetch.mdx) instead of assuming every installation can read the same keys. ## Key design diff --git a/versioned_docs/version-0.12/guides/best-practices/redis.md b/versioned_docs/version-0.12/guides/best-practices/redis.md index 5faadc2c..a12dffa7 100644 --- a/versioned_docs/version-0.12/guides/best-practices/redis.md +++ b/versioned_docs/version-0.12/guides/best-practices/redis.md @@ -10,6 +10,8 @@ Each installation of your app has its own Redis storage. An app installed on one This is the right model for most community games, mod tools, settings, leaderboards, and user state. Build with the assumption that each subreddit installation is its own environment. +If your app stores user content from Reddit, make sure you remove it from your app when that content is deleted from Reddit. Devvit provides post and comment delete events through triggers to help with this. See [Enable and respect user deletions](../../devvit_rules.md#enable-and-respect-user-deletions) in the Devvit Rules. + If your app needs cross-community data, like a global leaderboard or analytics rollup, plan for that directly. Use a shared service through [HTTP Fetch](../../capabilities/server/http-fetch.mdx) instead of assuming every installation can read the same keys. ## Key design diff --git a/versioned_docs/version-0.13/guides/best-practices/redis.md b/versioned_docs/version-0.13/guides/best-practices/redis.md index 5faadc2c..a12dffa7 100644 --- a/versioned_docs/version-0.13/guides/best-practices/redis.md +++ b/versioned_docs/version-0.13/guides/best-practices/redis.md @@ -10,6 +10,8 @@ Each installation of your app has its own Redis storage. An app installed on one This is the right model for most community games, mod tools, settings, leaderboards, and user state. Build with the assumption that each subreddit installation is its own environment. +If your app stores user content from Reddit, make sure you remove it from your app when that content is deleted from Reddit. Devvit provides post and comment delete events through triggers to help with this. See [Enable and respect user deletions](../../devvit_rules.md#enable-and-respect-user-deletions) in the Devvit Rules. + If your app needs cross-community data, like a global leaderboard or analytics rollup, plan for that directly. Use a shared service through [HTTP Fetch](../../capabilities/server/http-fetch.mdx) instead of assuming every installation can read the same keys. ## Key design