Skip to content

feat: remote functions cache API#15678

Draft
dummdidumm wants to merge 1 commit intomainfrom
remote-functions-cache
Draft

feat: remote functions cache API#15678
dummdidumm wants to merge 1 commit intomainfrom
remote-functions-cache

Conversation

@dummdidumm
Copy link
Copy Markdown
Member

@dummdidumm dummdidumm commented Apr 8, 2026

Adds a new remote functions cache API. Simple example:

/// file: src/routes/data.remote.js
import { query, command } from '$app/server';

export const getFastData = query(async () => {
	const { cache } = getRequestEvent();
	cache('100s');
	return { data: '...' };
});

export const updateData = command(async () => {
	// invalidates getFastData;
	// the next time someone requests it, it will be called again
	getFastData().invalidate();
});

For more info see the updated docs.

This is very WIP, and the adapter part isn't implemented yet (there are a few ways to approach it and we need to agree on the other APIs first). But it workds in dev and preview (public cache is implemented as runtime cache which very likely needs some more hardening).

TODOs/open questions:

  • right now event.cache() is "last one wins" except for tags which are merged. After sitting with it for a while this is I think the most straightforward and understandeable solution, but we can also approach it differently.
    • there could be one entry for public and one for private, i.e. you can have both. Drawback is that you might accidentally cache something publicly that you want to keep private
    • other way around: as soon as something is declared private it cannot become public anymore. Drawback is that you might call a private remote function from a public remote function, being fully aware of that and you only use secure parts of the privately-cached remote function so you are limited by the framework's decision now
    • error if you have both private and public. My least favorite option because we should recover gracefully; if that's your favorite you'd rather go "use private and ignore public; maybe have a warning at dev time"
    • same options for ttl and stale
  • is ttl and stale descriptive enough? Should it be maxAge and swr instead (closer to the web cache nomenclature)?
  • how to best integrate this with adapters? either they provide a file with some exports which are like hooks which we call at specific points (setHeaders, invalidate etc) or we don't do anything and do this purely via headers, and adapters can check these headers and either do runtime cache based on it and/or add cdn cache headers (though maybe they have to clone the response then; not sure how much of an overhead that is and if that matters)
  • this only works for remote functions right now, and it only works when you are calling them from the client. We could additionally have a SvelteKit-native runtime cache for public caching, and/or the adapter can hook into this to cache somewhere else than in memory (Vercel can use runtime cache, CF can use their cache, etc; i.e. this is related to the question above). This way we get more cache hits between client/server calls (or rather, we can get full page request cache this way, which we don't have at all right now).
  • can this be enhanced in a way that this is usable for full page requests, too (e.g. inside handle hook?). Private cache doesn't make sense there at least. I'd say it's possible to implement and would be intuitive with this API (we can say "do this in handle or load", or "assuming you use remote functions only we take the lowest cache across all of them as the page cache", etc etc, many possibilities) but we should do that later and not bother with it now.

Adds a new remote functions cache API. Simple example:

```ts
/// file: src/routes/data.remote.js
import { query, command } from '$app/server';

export const getFastData = query(async () => {
	const { cache } = getRequestEvent();
	cache('100s');
	return { data: '...' };
});

export const updateData = command(async () => {
	// invalidates getFastData;
	// the next time someone requests it, it will be called again
	getFastData().invalidate();
});
```

For more info see the updated docs.

This is very WIP, and the adapter part isn't implemented yet (there are a few ways to approach it and we need to agree on the other APIs first). But it workds in dev and preview (public cache is implemented as runtime cache which very likely needs some more hardening).

TODOs/open questions:
- right now `event.cache()` is "last one wins" except for tags which are merged. It probably makes sense to allow one entry for public cache and one for private, and either do "last one wins" or "lowest value wins"
- is `ttl` and `stale` descriptive enough? Should it be `maxAge` and `swr` instead (closer to the web cache nomenclature)?
- how to best integrate this with adapters? either they provide a file with some exports which are like hooks which we call at specific points (`setHeaders`, `invalidate` etc) or we don't do anything and do this purely via headers, and adapters can check these headers and either do runtime cache based on it and/or add cdn cache headers (though maybe they have to clone the response then; not sure how much of an overhead that is and if that matters)
- this only works for remote functions right now, and it only works when you are calling them from the client. We could additionally have a SvelteKit-native runtime cache for public caching, and/or the adapter can hook into this to cache somewhere else than in memory (Vercel can use runtime cache, CF can use their cache, etc; i.e. this is related to the question above). This way we get more cache hits between client/server calls (or rather, we can get full page request cache this way, which we don't have at all right now).
- can this be enhanced in a way that this is usable for full page requests, too (e.g. inside handle hook?). Private cache doesn't make sense there at least. I'd say it's possible to implement and would be intuitive with this API (we can say "do this in handle or load", or "assuming you use remote functions only we take the lowest cache across all of them as the page cache", etc etc, many possibilities) but we should do that later and not bother with it now.
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 8, 2026

🦋 Changeset detected

Latest commit: 7557320

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@sveltejs/kit Minor
@sveltejs/adapter-node Patch
@sveltejs/adapter-vercel Patch
@sveltejs/adapter-netlify Patch
@sveltejs/adapter-cloudflare Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@svelte-docs-bot
Copy link
Copy Markdown

// shareable across users (CDN caching) or private to user (browser caching); default private
scope: 'private',
// used for invalidation, when not given is the URL
tags: ['my-data'],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the URL is the right cache key here... it should probably be the remote function key instead. Also, tags should be additive, not replace the default key.

@elliott-with-the-longest-name-on-github
Copy link
Copy Markdown
Contributor

One thing I don't think is quite right here: Caching remote functions requires two coordinated layers of caching. One of them is a runtime cache at the server level, which needs to be used to cache direct-from-server remote function calls. This cache should be granular -- i.e. if I call getUser and getTeam and they have separate cache durations, that's fine -- they can be cached separately. The other is the "request-level" cache, which basically needs to take the lowest common denominator TTL and apply it to the request.

Overall, I really think SvelteKit core should not do anything with the information from the cache API -- instead, the adapters should do everything:

  • Adapters should provide a get(key: string): Promise<string | undefined> function, which SvelteKit calls for every query
  • Adapters should provide invalidate(key: string): Promise<void> and invalidateTag(tag: string): Promise<void> functions, which SvelteKit delegates to for
  • Adapters should receive a Map<string, CacheInvocation> object that they can use to do... whatever they want. For example, the Vercel adapter would likely map over this and cache everything in the runtime cache, and, if the request is a remote request for a query endpoint, it would consolidate the runtime cache TTLs to find the soonest-to-be-invalidated entry and set the overall request cache time to that

* Options for [`event.cache`](https://svelte.dev/docs/kit/@sveltejs-kit#RequestEvent)
*/
export interface CacheOptions {
ttl: string | number;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be a type like

`${number}d`  | `${number}h` | `${number}m` | `${number}s`  | `${number}ms`

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ms/day don't really make sense imo but otherwise yes; on my list of todos once we agree on the API 👍

@ottomated
Copy link
Copy Markdown
Contributor

Would cache('immutable') be an accepted option? i.e. no ttl, only gets invalidated manually

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants