Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cloud_docs/concepts/logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ Two environment variables control session logging. With a database enabled (the

To change either default, set the value with `scloud variable set`. See [Passwords, secrets, and environment variables](/cloud/concepts/passwords-secrets-env-vars) for variable management.

For what the server records, which tables it writes to, and how retention works, see [Logging](https://docs.serverpod.dev/concepts/operations/logging) in the framework documentation.

## Troubleshooting

**Invalid timestamp format.** Use ISO 8601 form (`YYYY-MM-DDTHH:MM:SSZ`) or a supported duration string (`5m`, `2h`, `1d`).
Expand Down
4 changes: 2 additions & 2 deletions cloud_docs/guides/redis.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ After a successful deploy, your server will use Upstash for Redis-backed caching

## Related

- [Serverpod configuration](https://docs.serverpod.dev/concepts/configuration) for Redis options and environment variables.
- [Serverpod caching](https://docs.serverpod.dev/concepts/caching) for local and Redis-backed caches.
- [Serverpod configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration) for Redis options and environment variables.
- [Serverpod caching](https://docs.serverpod.dev/concepts/endpoints-and-apis/caching) for local and Redis-backed caches.
- [Upstash: Connect your client](https://upstash.com/docs/redis/howto/connectclient) for connection details and TLS.
- [Passwords, secrets, and environment variables](/cloud/concepts/passwords-secrets-env-vars) for how Serverpod Cloud injects passwords and variables.
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,33 @@ The server accepts arguments that control how it starts: `--mode` selects the ru
In production, the `--role` argument controls which parts of the server run:

- **`monolith`** (default) runs everything: the API, Insights, and web servers, plus [future calls](../scheduling/overview) and [health checks](../operations/health-checks).
- **`serverless`** serves requests only. Future calls and health checks are disabled, which fits platforms that start and stop instances on demand.
- **`serverless`** serves requests only. Future calls and [health metric collection](../operations/health-checks#health-metrics) are disabled, which fits platforms that start and stop instances on demand. The health probes still answer.
- **`maintenance`** starts no servers. It performs one-shot work, applying migrations when passed `--apply-migrations` and running any due future calls, then exits. The exit code reports success or failure, which makes it fit for CI jobs and scheduled maintenance tasks.

## Run code on shutdown

Register a shutdown task to do cleanup work when the server stops, such as flushing state or releasing an external resource. Tasks run after the server stops accepting requests but before the database and Redis connections close, so they can still use them.

:::warning
Shutdown tasks are an experimental API and can change in a breaking way in any minor release.
:::

Register them on the `Serverpod` instance in `lib/server.dart`, where you create it:

```dart
pod.experimental.shutdownTasks.addTask(#taskIdentifier, () async {
// Your shutdown logic here.
});
```

Each task is registered under an identifier you choose, used in log messages and to remove the task again. Any object works; `#taskIdentifier` above is a Dart symbol, which is a convenient way to write a constant name.

```dart
pod.experimental.shutdownTasks.removeTask(#taskIdentifier);
```

Registering two tasks under the same identifier throws a `StateError`. All tasks run concurrently, and the server waits for them all before shutting down. A task that throws does not stop the shutdown, but the error is logged and the process exits with a non-zero code, which a host reading exit status will treat as a failed shutdown.

## Related

- [`serverpod start` reference](../cli/commands/start): every command-line option.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ experimental_features:
all: true
```

See the [experimental features documentation](../operations/experimental-features) for details.
See [Exception monitoring](../operations/exception-monitoring) for the experimental features Serverpod currently exposes.

:::warning
Experimental features may change or be removed in future versions.
Expand Down
2 changes: 1 addition & 1 deletion docs/06-concepts/02-endpoints-and-apis/02-sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class ExampleEndpoint extends Endpoint {
| Member | What it is |
| --------------------------- | ----------------------------------------------------------------------------------------------- |
| `db` | Database access. See the [CRUD docs](../data-and-the-database/database/crud). |
| `caches` | Local and distributed caching. See [caching](../operations/caching). |
| `caches` | Local and distributed caching. See [caching](./caching). |
| `storage` | File storage. See [file uploads](./file-uploads). |
| `messages` | Server events for real-time communication. See [server events](./server-events). |
| `passwords` | Secrets from config and environment. See [configuration](../server-fundamentals/configuration). |
Expand Down
153 changes: 153 additions & 0 deletions docs/06-concepts/02-endpoints-and-apis/06-caching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
---
description: A cache stores a result under a key so the next request can read it back, held in server memory or in Redis and reached through session.caches.
---

# Caching

Some work is expensive to repeat: a query that joins several tables, a result assembled from many rows, or a value fetched from another service. Caching stores the result under a key so the next request can read it back instead of doing the work again.

Serverpod's caches are reached from the [`Session`](./sessions) object, alongside the database and messaging. Anything you cache must be serializable: the primitives (`int`, `String`, `bool`, `double`, `DateTime`, `Duration`, `ByteData`, `UuidValue`, `Uri`, `BigInt`), collections of them, and your own [models](../data-and-the-database/models). Use a model rather than a hand-written class: one with a `toJson` stores fine and throws on read, one without fails immediately with `JsonUnsupportedObjectError`.

## The caches

| Cache | Where it lives | Use it for |
| --- | --- | --- |
| `session.caches.local` | Memory of the server handling this call | The default choice for cached values. |
| `session.caches.localPrio` | Memory of the same server, kept separate | Values you want to keep hot, held apart from ordinary entries. |
| `session.caches.global` | Redis, shared by every server instance | Values that must be the same on every instance. |

There is a fourth cache, `session.caches.query`, which Serverpod manages for cacheable database queries. It is not a place to put your own objects.

Each local cache holds at most 10,000 entries. When a cache is full, the oldest entry is dropped to make room, based on when it was inserted rather than when it was last read, so a frequently read key can still be evicted.

## Cache an object

Read a value with `get`, and write one with `put`:

```dart
Future<UserData?> getUserData(Session session, int userId) async {
var cacheKey = 'UserData-$userId';

// Try the cache first.
var userData = await session.caches.local.get<UserData>(cacheKey);
if (userData != null) return userData;

// Not cached, so load it and store it for five minutes.
userData = await UserData.db.findById(session, userId);
if (userData != null) {
await session.caches.local.put(
cacheKey,
userData,
lifetime: const Duration(minutes: 5),
);
}

return userData;
}
```

The `lifetime` sets how long the entry stays valid. After it elapses, the next read misses and returns `null`. Omit `lifetime` and the entry never expires on its own, so it stays until you invalidate it or it is evicted to make room.

Cache `DateTime` values as UTC, since that is how they come back:

```dart
await session.caches.local.put('lastUpdate', DateTime.now().toUtc());

var cached = await session.caches.local.get<DateTime>('lastUpdate');
```

Collections work the same way. Give `get` the full type, including what the collection holds:

```dart
await session.caches.local.put('users', [alice, bob]);

var users = await session.caches.local.get<List<UserData>>('users');
```

Serverpod generates the code to read a collection type back only when your project uses it, such as in an endpoint signature or a model field. Caching a shape that appears nowhere else in your project throws on read.

### Load on a miss

The `CacheMissHandler` class folds "read it, and compute it if it is missing" into one call. Serverpod stores whatever the handler returns:

```dart
Future<UserData?> getUserData(Session session, int userId) async {
return session.caches.local.get(
'UserData-$userId',
CacheMissHandler(
() async => UserData.db.findById(session, userId),
lifetime: const Duration(minutes: 5),
),
);
}
```

The call returns `null` when the handler does, and nothing is stored in that case. If several requests miss the same key at once, the local caches run the handler once and let the others wait for that result. The Redis-backed global cache runs it per request.

## Invalidate entries

A cached value goes stale as soon as the underlying data changes, so remove it when you write:

```dart
await UserData.db.updateRow(session, userData);
await session.caches.local.invalidateKey('UserData-${userData.id}');
```

To drop several related entries at once, put them in a group and invalidate the group:

```dart
await session.caches.local.put(
'UserData-$userId',
userData,
group: 'user-$userId',
);

// Later, remove every entry in the group.
await session.caches.local.invalidateGroup('user-$userId');
```

Use `containsKey` to check for an entry without reading it, and `clear` to empty the cache.

:::warning
Groups are a local-cache feature. On `session.caches.global`, passing `group:` to `put` or calling `invalidateGroup` throws `UnimplementedError`, so code that groups entries breaks when you move it to the global cache.
:::

## The global cache and Redis

The global cache uses Redis, which is what makes it shared between server instances. When Redis is not configured, it falls back to an in-memory cache that belongs to a single server process.

:::warning
The fallback applies in every run mode, production included. It exists so you can run code that uses the global cache without a Redis server during development, not as a substitute for Redis in production. In that state each instance keeps its own copy, nothing is shared between instances, and everything is lost on restart.

The server does not warn you about this at normal logging levels, and the [readiness probe](../operations/health-checks) still passes, so confirm your Redis configuration before running more than one instance. If Redis is configured but unreachable, the readiness probe fails, so that case does get caught.
:::

Redis counts as configured when your config has a `redis` section that is enabled. A `redis` section that omits `enabled` is treated as enabled. If the section is present but its password is missing, the server fails to start rather than falling back.

Serverpod does not manage the size of the global cache or decide what it evicts. Redis does, according to how you have configured it, so set a memory limit and an eviction policy there if you cache a large number of objects globally.

Serverpod Cloud does not provide a managed Redis instance, so a deployed app uses the in-memory fallback unless you connect your own. See the [Redis guide](/cloud/guides/redis) for how to set one up.

## Send Redis commands directly

Redis supports operations the cache API does not expose, such as atomic counters and sorted sets. You can borrow the connection Serverpod manages:

```dart
Future<int?> incrementCounter(Session session, String counterName) async {
var command = await session.serverpod.redisController?.getConnection();
if (command == null) return null;

var result = await command.send_object(['INCR', 'my_app:$counterName']);
return result is int ? result : null;
}
```

The connection is `null` when Redis is disabled or unreachable, including when the global cache is on its in-memory fallback. Responses come back in the shape Redis defines for each command, so check the result before casting it.

The returned `RedisCommand` is Serverpod's own connection: do not close it, and do not replace `Serverpod.redisController`. Namespace your keys so they cannot collide with Serverpod's own cache entries.

## Related

- [Sessions](./sessions): the object the caches hang off.
- [Server events](./server-events): the other Redis-backed feature, for messaging between instances.
- [Configuration](../server-fundamentals/configuration): where the `redis` section lives.
123 changes: 0 additions & 123 deletions docs/06-concepts/07-operations/01-caching.md

This file was deleted.

Loading
Loading