diff --git a/package.json b/package.json
index de61ae7f9f5..452aad332c0 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
   },
   "pnpm": {
     "overrides": {
-      "caniuse-lite": "1.0.30001577"
+      "caniuse-lite": "1.0.30001636"
     }
   },
   "devDependencies": {
diff --git a/packages/cli/src/benchmark.ts b/packages/cli/src/benchmark.ts
index 83967681f41..2db1ea5cca4 100644
--- a/packages/cli/src/benchmark.ts
+++ b/packages/cli/src/benchmark.ts
@@ -489,6 +489,7 @@ export const benchmarkCommand = async (
 						}).value,
 					compositionStart: 0,
 					onBrowserDownload,
+					onArtifact: () => undefined,
 				},
 				(run, progress) => {
 					benchmarkProgress.update(
diff --git a/packages/cli/src/on-artifact.ts b/packages/cli/src/on-artifact.ts
new file mode 100644
index 00000000000..0ed587898e7
--- /dev/null
+++ b/packages/cli/src/on-artifact.ts
@@ -0,0 +1,51 @@
+import type {OnArtifact} from '@remotion/renderer';
+import type {ArtifactProgress} from '@remotion/studio-shared';
+import {existsSync, mkdirSync, writeFileSync} from 'fs';
+import path from 'path';
+
+export const handleOnArtifact = ({
+	artifactState,
+	onProgress,
+	compositionId,
+}: {
+	artifactState: ArtifactProgress;
+	onProgress: (artifact: ArtifactProgress) => void;
+	compositionId: string;
+}) => {
+	const initialProgress = {...artifactState};
+
+	const onArtifact: OnArtifact = (artifact) => {
+		// It would be nice in the future to customize the artifact output destination
+
+		const relativeOutputDestination = path.join(
+			'out',
+			compositionId,
+			artifact.filename.replace('/', path.sep),
+		);
+		const defaultOutName = path.join(process.cwd(), relativeOutputDestination);
+
+		if (!existsSync(path.dirname(defaultOutName))) {
+			mkdirSync(path.dirname(defaultOutName), {
+				recursive: true,
+			});
+		}
+
+		const alreadyExisted = existsSync(defaultOutName);
+
+		writeFileSync(defaultOutName, artifact.content);
+
+		initialProgress.received.push({
+			absoluteOutputDestination: defaultOutName,
+			filename: artifact.filename,
+			sizeInBytes: artifact.content.length,
+			alreadyExisted,
+			relativeOutputDestination,
+		});
+	};
+
+	onProgress(initialProgress);
+
+	return {
+		onArtifact,
+	};
+};
diff --git a/packages/cli/src/progress-bar.ts b/packages/cli/src/progress-bar.ts
index cc57ecd88ea..8a15af1fcc5 100644
--- a/packages/cli/src/progress-bar.ts
+++ b/packages/cli/src/progress-bar.ts
@@ -8,6 +8,7 @@ import type {
 	StitchingProgressInput,
 } from '@remotion/studio-server';
 import {StudioServerInternals} from '@remotion/studio-server';
+import {formatBytes, type ArtifactProgress} from '@remotion/studio-shared';
 import {chalk} from './chalk';
 import {
 	getFileSizeDownloadBar,
@@ -192,6 +193,30 @@ const makeRenderingProgress = ({
 		.join(' ');
 };
 
+const makeArtifactProgress = (artifactState: ArtifactProgress) => {
+	const {received} = artifactState;
+	if (received.length === 0) {
+		return null;
+	}
+
+	return received
+		.map((artifact) => {
+			return [
+				chalk.blue((artifact.alreadyExisted ? '○' : '+').padEnd(LABEL_WIDTH)),
+				chalk.blue(
+					makeHyperlink({
+						url: 'file://' + artifact.absoluteOutputDestination,
+						fallback: artifact.absoluteOutputDestination,
+						text: artifact.relativeOutputDestination,
+					}),
+				),
+				chalk.gray(`${formatBytes(artifact.sizeInBytes)}`),
+			].join(' ');
+		})
+		.filter(truthy)
+		.join('\n');
+};
+
 export const getRightLabelWidth = (totalFrames: number) => {
 	return `${totalFrames}/${totalFrames}`.length;
 };
@@ -237,7 +262,7 @@ export const makeRenderingAndStitchingProgress = ({
 	progress: number;
 	message: string;
 } => {
-	const {rendering, stitching, downloads, bundling} = prog;
+	const {rendering, stitching, downloads, bundling, artifactState} = prog;
 	const output = [
 		rendering ? makeRenderingProgress(rendering) : null,
 		makeMultiDownloadProgress(downloads, rendering?.totalFrames ?? 0),
@@ -247,6 +272,7 @@ export const makeRenderingAndStitchingProgress = ({
 					stitchingProgress: stitching,
 					isUsingParallelEncoding,
 				}),
+		makeArtifactProgress(artifactState),
 	]
 		.filter(truthy)
 		.join('\n');
diff --git a/packages/cli/src/progress-types.ts b/packages/cli/src/progress-types.ts
index 58fe3bb4a5d..497f4d07662 100644
--- a/packages/cli/src/progress-types.ts
+++ b/packages/cli/src/progress-types.ts
@@ -12,4 +12,7 @@ export const initialAggregateRenderProgress = (): AggregateRenderProgress => ({
 		bytes: 0,
 		doneIn: null,
 	},
+	artifactState: {
+		received: [],
+	},
 });
diff --git a/packages/cli/src/render-flows/render.ts b/packages/cli/src/render-flows/render.ts
index f3493511577..b657c9e2602 100644
--- a/packages/cli/src/render-flows/render.ts
+++ b/packages/cli/src/render-flows/render.ts
@@ -28,6 +28,7 @@ import type {
 	RenderingProgressInput,
 	StitchingProgressInput,
 } from '@remotion/studio-server';
+import type {ArtifactProgress} from '@remotion/studio-shared';
 import fs, {existsSync} from 'node:fs';
 import os from 'node:os';
 import path from 'node:path';
@@ -42,6 +43,7 @@ import {makeHyperlink} from '../hyperlinks/make-link';
 import {getVideoImageFormat} from '../image-formats';
 import {Log} from '../log';
 import {makeOnDownload} from '../make-on-download';
+import {handleOnArtifact} from '../on-artifact';
 import {parsedCli, quietFlagProvided} from '../parsed-cli';
 import {
 	LABEL_WIDTH,
@@ -219,6 +221,8 @@ export const renderVideoFlow = async ({
 		doneIn: null,
 	};
 
+	let artifactState: ArtifactProgress = {received: []};
+
 	const updateRenderProgress = ({
 		newline,
 		printToConsole,
@@ -232,6 +236,7 @@ export const renderVideoFlow = async ({
 			downloads,
 			bundling: bundlingProgress,
 			copyingState,
+			artifactState,
 		};
 
 		const {output, message, progress} = makeRenderingAndStitchingProgress({
@@ -322,6 +327,15 @@ export const renderVideoFlow = async ({
 			onBrowserDownload,
 		});
 
+	const {onArtifact} = handleOnArtifact({
+		artifactState,
+		onProgress: (progress) => {
+			artifactState = progress;
+			updateRenderProgress({newline: false, printToConsole: true});
+		},
+		compositionId,
+	});
+
 	const {value: codec, source: codecReason} =
 		BrowserSafeApis.options.videoCodecOption.getValue(
 			{
@@ -429,6 +443,7 @@ export const renderVideoFlow = async ({
 		codec: shouldOutputImageSequence ? undefined : codec,
 		uiImageFormat,
 	});
+
 	if (shouldOutputImageSequence) {
 		fs.mkdirSync(absoluteOutputFile, {
 			recursive: true,
@@ -491,6 +506,7 @@ export const renderVideoFlow = async ({
 			compositionStart: 0,
 			forSeamlessAacConcatenation,
 			onBrowserDownload,
+			onArtifact,
 		});
 
 		Log.info({indent, logLevel}, chalk.blue(`▶ ${absoluteOutputFile}`));
@@ -580,6 +596,7 @@ export const renderVideoFlow = async ({
 		forSeamlessAacConcatenation,
 		compositionStart: 0,
 		onBrowserDownload,
+		onArtifact,
 	});
 	if (!updatesDontOverwrite) {
 		updateRenderProgress({newline: true, printToConsole: true});
diff --git a/packages/cli/src/render-flows/still.ts b/packages/cli/src/render-flows/still.ts
index 3406481e2cd..f538dc667f9 100644
--- a/packages/cli/src/render-flows/still.ts
+++ b/packages/cli/src/render-flows/still.ts
@@ -27,6 +27,7 @@ import {getCompositionWithDimensionOverride} from '../get-composition-with-dimen
 import {makeHyperlink} from '../hyperlinks/make-link';
 import {Log} from '../log';
 import {makeOnDownload} from '../make-on-download';
+import {handleOnArtifact} from '../on-artifact';
 import {parsedCli, quietFlagProvided} from '../parsed-cli';
 import type {OverwriteableCliOutput} from '../progress-bar';
 import {
@@ -124,15 +125,13 @@ export const renderStillFlow = async ({
 	const updateRenderProgress = ({
 		newline,
 		printToConsole,
-		isUsingParallelEncoding,
 	}: {
 		newline: boolean;
 		printToConsole: boolean;
-		isUsingParallelEncoding: boolean;
 	}) => {
 		const {output, progress, message} = makeRenderingAndStitchingProgress({
 			prog: aggregate,
-			isUsingParallelEncoding,
+			isUsingParallelEncoding: false,
 		});
 		if (printToConsole) {
 			renderProgress.update(updatesDontOverwrite ? message : output, newline);
@@ -176,7 +175,6 @@ export const renderStillFlow = async ({
 				updateRenderProgress({
 					newline: false,
 					printToConsole: true,
-					isUsingParallelEncoding: false,
 				});
 			},
 			indentOutput: indent,
@@ -309,7 +307,6 @@ export const renderStillFlow = async ({
 	updateRenderProgress({
 		newline: false,
 		printToConsole: true,
-		isUsingParallelEncoding: false,
 	});
 
 	const onDownload: RenderMediaOnDownload = makeOnDownload({
@@ -321,6 +318,19 @@ export const renderStillFlow = async ({
 		isUsingParallelEncoding: false,
 	});
 
+	const {onArtifact} = handleOnArtifact({
+		artifactState: aggregate.artifactState,
+		compositionId,
+		onProgress: (progress) => {
+			aggregate.artifactState = progress;
+
+			updateRenderProgress({
+				newline: false,
+				printToConsole: true,
+			});
+		},
+	});
+
 	await RenderInternals.internalRenderStill({
 		composition: config,
 		frame: stillFrame,
@@ -352,6 +362,7 @@ export const renderStillFlow = async ({
 		offthreadVideoCacheSizeInBytes,
 		binariesDirectory,
 		onBrowserDownload,
+		onArtifact,
 	});
 
 	aggregate.rendering = {
@@ -363,7 +374,6 @@ export const renderStillFlow = async ({
 	updateRenderProgress({
 		newline: true,
 		printToConsole: true,
-		isUsingParallelEncoding: false,
 	});
 	Log.info(
 		{indent, logLevel},
diff --git a/packages/cloudrun/src/functions/render-media-single-thread.ts b/packages/cloudrun/src/functions/render-media-single-thread.ts
index 18133e9a3ea..2b977ee6f71 100644
--- a/packages/cloudrun/src/functions/render-media-single-thread.ts
+++ b/packages/cloudrun/src/functions/render-media-single-thread.ts
@@ -1,6 +1,10 @@
 import type * as ff from '@google-cloud/functions-framework';
 import {Storage} from '@google-cloud/storage';
-import type {ChromiumOptions, RenderMediaOnProgress} from '@remotion/renderer';
+import type {
+	ChromiumOptions,
+	OnArtifact,
+	RenderMediaOnProgress,
+} from '@remotion/renderer';
 import {RenderInternals} from '@remotion/renderer';
 import {NoReactInternals} from 'remotion/no-react';
 import {VERSION} from 'remotion/version';
@@ -60,6 +64,10 @@ export const renderMediaSingleThread = async (
 			enableMultiProcessOnLinux: true,
 		};
 
+		const onArtifact: OnArtifact = () => {
+			throw new Error('Emitting artifacts is not supported in Cloud Run');
+		};
+
 		await RenderInternals.internalRenderMedia({
 			composition: {
 				...composition,
@@ -127,6 +135,7 @@ export const renderMediaSingleThread = async (
 			onBrowserDownload: () => {
 				throw new Error('Should not download a browser in Cloud Run');
 			},
+			onArtifact,
 		});
 
 		const storage = new Storage();
diff --git a/packages/cloudrun/src/functions/render-still-single-thread.ts b/packages/cloudrun/src/functions/render-still-single-thread.ts
index d822f768cb4..a650953d8e3 100644
--- a/packages/cloudrun/src/functions/render-still-single-thread.ts
+++ b/packages/cloudrun/src/functions/render-still-single-thread.ts
@@ -98,6 +98,9 @@ export const renderStillSingleThread = async (
 			onBrowserDownload: () => {
 				throw new Error('Should not download a browser in Cloud Run');
 			},
+			onArtifact: () => {
+				throw new Error('Emitting artifacts is not supported in Cloud Run');
+			},
 		});
 		Log.info({indent: false, logLevel: body.logLevel}, 'Still rendered');
 
diff --git a/packages/core/src/Artifact.tsx b/packages/core/src/Artifact.tsx
new file mode 100644
index 00000000000..673e19fb786
--- /dev/null
+++ b/packages/core/src/Artifact.tsx
@@ -0,0 +1,61 @@
+import type React from 'react';
+import {useContext, useEffect, useState} from 'react';
+import {RenderAssetManager} from './RenderAssetManager';
+import {getRemotionEnvironment} from './get-remotion-environment';
+import {useCurrentFrame} from './use-current-frame';
+
+export const Artifact: React.FC<{
+	readonly filename: string;
+	readonly content: string | Uint8Array;
+}> = ({filename, content}) => {
+	const {registerRenderAsset, unregisterRenderAsset} =
+		useContext(RenderAssetManager);
+
+	const [env] = useState(() => getRemotionEnvironment());
+
+	const frame = useCurrentFrame();
+
+	const [id] = useState(() => {
+		return String(Math.random());
+	});
+
+	useEffect(() => {
+		if (!env.isRendering) {
+			return;
+		}
+
+		if (content instanceof Uint8Array) {
+			registerRenderAsset({
+				type: 'artifact',
+				id,
+				content: btoa(new TextDecoder('utf8').decode(content)),
+				filename,
+				frame,
+				binary: true,
+			});
+		} else {
+			registerRenderAsset({
+				type: 'artifact',
+				id,
+				content,
+				filename,
+				frame,
+				binary: false,
+			});
+		}
+
+		return () => {
+			return unregisterRenderAsset(id);
+		};
+	}, [
+		content,
+		env.isRendering,
+		filename,
+		frame,
+		id,
+		registerRenderAsset,
+		unregisterRenderAsset,
+	]);
+
+	return null;
+};
diff --git a/packages/core/src/CompositionManager.tsx b/packages/core/src/CompositionManager.tsx
index bc83c93337a..24ebe63ebb9 100644
--- a/packages/core/src/CompositionManager.tsx
+++ b/packages/core/src/CompositionManager.tsx
@@ -126,7 +126,7 @@ export type TSequence = {
 	premountDisplay: number | null;
 } & EnhancedTSequenceData;
 
-export type TRenderAsset = {
+export type AudioOrVideoAsset = {
 	type: 'audio' | 'video';
 	src: string;
 	id: string;
@@ -139,6 +139,17 @@ export type TRenderAsset = {
 	audioStartFrame: number;
 };
 
+export type ArtifactAsset = {
+	type: 'artifact';
+	id: string;
+	filename: string;
+	content: string | Uint8Array;
+	frame: number;
+	binary: boolean;
+};
+
+export type TRenderAsset = AudioOrVideoAsset | ArtifactAsset;
+
 export const compositionsRef = React.createRef<{
 	getCompositions: () => AnyComposition[];
 }>();
diff --git a/packages/core/src/RenderAssetManager.tsx b/packages/core/src/RenderAssetManager.tsx
index 43588b15e6a..87b41d67dad 100644
--- a/packages/core/src/RenderAssetManager.tsx
+++ b/packages/core/src/RenderAssetManager.tsx
@@ -6,6 +6,7 @@ import {
 	useState,
 } from 'react';
 import type {TRenderAsset} from './CompositionManager.js';
+import {validateRenderAsset} from './validation/validate-artifact.js';
 
 export type RenderAssetManagerContext = {
 	registerRenderAsset: (renderAsset: TRenderAsset) => void;
@@ -26,6 +27,7 @@ export const RenderAssetManagerProvider: React.FC<{
 	const [renderAssets, setRenderAssets] = useState<TRenderAsset[]>([]);
 
 	const registerRenderAsset = useCallback((renderAsset: TRenderAsset) => {
+		validateRenderAsset(renderAsset);
 		setRenderAssets((assets) => {
 			return [...assets, renderAsset];
 		});
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index a083bc3af17..88ed8ff801b 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -91,6 +91,7 @@ export type BundleState =
 
 checkMultipleRemotionVersions();
 export * from './AbsoluteFill.js';
+export {Artifact} from './Artifact.js';
 export * from './audio/index.js';
 export {cancelRender} from './cancel-render.js';
 export {
@@ -103,6 +104,7 @@ export {
 export {
 	AnyCompMetadata,
 	AnyComposition,
+	AudioOrVideoAsset,
 	SmallTCompMetadata,
 	TCompMetadata,
 	TRenderAsset,
diff --git a/packages/core/src/internals.ts b/packages/core/src/internals.ts
index 656c475d3f8..3a9342b779d 100644
--- a/packages/core/src/internals.ts
+++ b/packages/core/src/internals.ts
@@ -75,6 +75,7 @@ import {
 import {useLazyComponent} from './use-lazy-component.js';
 import {useUnsafeVideoConfig} from './use-unsafe-video-config.js';
 import {useVideo} from './use-video.js';
+import {validateRenderAsset} from './validation/validate-artifact.js';
 import {
 	invalidCompositionErrorMessage,
 	isCompositionIdValid,
@@ -161,6 +162,7 @@ export const Internals = {
 	calculateScale,
 	editorPropsProviderRef,
 	PROPS_UPDATED_EXTERNALLY,
+	validateRenderAsset,
 } as const;
 
 export type {
diff --git a/packages/core/src/no-react.ts b/packages/core/src/no-react.ts
index 6a81eed547d..4559b5f4f72 100644
--- a/packages/core/src/no-react.ts
+++ b/packages/core/src/no-react.ts
@@ -1,4 +1,8 @@
-export type {TRenderAsset} from './CompositionManager';
+export type {
+	ArtifactAsset,
+	AudioOrVideoAsset,
+	TRenderAsset,
+} from './CompositionManager';
 export type {ClipRegion} from './NativeLayers';
 export {
 	EasingFunction,
diff --git a/packages/core/src/validation/validate-artifact.ts b/packages/core/src/validation/validate-artifact.ts
new file mode 100644
index 00000000000..f3f4232b291
--- /dev/null
+++ b/packages/core/src/validation/validate-artifact.ts
@@ -0,0 +1,41 @@
+import type {TRenderAsset} from '../CompositionManager';
+
+export const validateArtifactFilename = (filename: unknown) => {
+	if (typeof filename !== 'string') {
+		throw new TypeError(
+			`The "filename" must be a string, but you passed a value of type ${typeof filename}`,
+		);
+	}
+
+	if (filename.trim() === '') {
+		throw new Error('The `filename` must not be empty');
+	}
+
+	if (!filename.match(/^([0-9a-zA-Z-!_.*'()/:&$@=;+,?]+)/g)) {
+		throw new Error(
+			'The `filename` must match "/^([0-9a-zA-Z-!_.*\'()/:&$@=;+,?]+)/g". Use forward slashes only, even on Windows.',
+		);
+	}
+};
+
+const validateContent = (content: unknown) => {
+	if (typeof content !== 'string' && !(content instanceof Uint8Array)) {
+		throw new TypeError(
+			`The "content" must be a string or Uint8Array, but you passed a value of type ${typeof content}`,
+		);
+	}
+
+	if (typeof content === 'string' && content.trim() === '') {
+		throw new Error('The `content` must not be empty');
+	}
+};
+
+export const validateRenderAsset = (artifact: TRenderAsset) => {
+	// We don't have validation for it yet
+	if (artifact.type !== 'artifact') {
+		return;
+	}
+
+	validateArtifactFilename(artifact.filename);
+	validateContent(artifact.content);
+};
diff --git a/packages/docs/docs/artifact.mdx b/packages/docs/docs/artifact.mdx
new file mode 100644
index 00000000000..b0cf31c288b
--- /dev/null
+++ b/packages/docs/docs/artifact.mdx
@@ -0,0 +1,63 @@
+---
+image: /generated/articles-docs-artifact.png
+title: "<Artifact>"
+crumb: API
+---
+
+# `<Artifact>`<AvailableFrom v="4.0.176"/>
+
+By rendering an `<Artifact>` tag in your Remotion markup, [an extra file will get emitted during rendering](/docs/artifacts).
+
+```tsx twoslash title="MyComp.tsx"
+import { Artifact, useCurrentFrame } from "remotion";
+
+export const MyComp: React.FC = () => {
+  const frame = useCurrentFrame();
+
+  return frame === 0 ? (
+    <Artifact filename="my-file.txt" content="Hello World!" />
+  ) : null;
+};
+```
+
+If rendered on the CLI or via the Studio, this will emit an additional file:
+
+```
+$ npx remotion render MyComp
++ out/MyComp.mp4
++ my-file.txt (12B)
+```
+
+It is allowed for a composition to emit multiple files.  
+However, the file names must be unique.
+
+The component will get evaluated on every frame, which means if you want to emit just one file, only render it on one frame.
+
+```tsx twoslash title="❌ Will generate an asset on every frame and throw an error because the file name is not unique"
+import { Artifact, useCurrentFrame } from "remotion";
+
+export const MyComp: React.FC = () => {
+  const frame = useCurrentFrame();
+
+  return frame === 0 ? (
+    <Artifact filename="my-file.txt" content="Hello World!" />
+  ) : null;
+};
+```
+
+## API
+
+### `filename`
+
+A string that is the name of the file that will be emitted.  
+Use forward slashes only, even on Windows.  
+Must match the regex `/^([0-9a-zA-Z-!_.*'()/:&$@=;+,?]+)/g`.
+
+### `content`
+
+A `string` or `Uint8Array` that is the content of the file that will be emitted. Don't consider an `Uint8Array` to be faster, because it needs to be serialized.
+
+## See also
+
+- [Emitting Artifacts](/docs/artifacts)
+- [Source code for this component](https://github.com/remotion-dev/remotion/blob/main/packages/core/src/components/Artifact.tsx)
diff --git a/packages/docs/docs/artifacts.mdx b/packages/docs/docs/artifacts.mdx
new file mode 100644
index 00000000000..619223cf28e
--- /dev/null
+++ b/packages/docs/docs/artifacts.mdx
@@ -0,0 +1,193 @@
+---
+image: /generated/articles-docs-artifacts.png
+title: Emitting Artifacts
+id: artifacts
+crumb: "Techniques"
+---
+
+# Emitting Artifacts<AvailableFrom v="4.0.176" />
+
+Sometimes you wish to generate additional files when rendering your video. For example:
+
+- A `.srt` subtitle file
+- A `.txt` containing chapters of the video
+- A `CREDITS` file for the assets used in the video
+- Debug information from the render.
+
+You can use the [`<Artifact>`](/docs/artifact) component to emit arbitrary files from your video.
+
+:::note
+Emitting artifacts is not currently supported by `@remotion/cloudrun`.
+:::
+
+## Example
+
+```tsx twoslash title="MyComp.tsx"
+// @filename: subtitles.tsx
+
+export const generateSubtitles = () => {
+  return ``;
+};
+// @filename: MyComp.tsx
+// ---cut---
+import React from "react";
+import { Artifact, useCurrentFrame } from "remotion";
+import { generateSubtitles } from "./subtitles";
+
+export const MyComp: React.FC = () => {
+  const frame = useCurrentFrame();
+  return (
+    <>
+      {frame === 0 ? (
+        <Artifact filename="captions.srt" content={generateSubtitles()} />
+      ) : null}
+    </>
+  );
+};
+```
+
+## Rules of artifacts
+
+<Step>1</Step> The asset should only be rendered for one single frame of the video.
+Otherwise, the asset will get emitted multiple times. <br />
+
+<Step>2</Step> It is possible to emit multiple assets, but they may not have the
+same filename.
+<br />
+
+<Step>3</Step> For the <code>content</code> prop it is possible to pass a <code>
+  string
+</code>
+, or a <code>Uint8Array</code> for binary data. Passing an <code>
+  Uint8Array
+</code> should not be considered faster due to it having to be serialized.
+<br />
+
+## Receiving artifacts
+
+### In the CLI or Studio
+
+Artifacts get saved to `out/[composition-id]/[filename]` when rendering a video.
+
+### Using `renderMedia()`, `renderStill()` or `renderFrames()`
+
+Use the [`onArtifact`](/docs/renderer/render-media#onartifact) callback to receive the artifacts.
+
+```tsx twoslash title="render.mjs"
+// @module: es2022
+// @target: es2017
+import type { VideoConfig } from "remotion";
+import fs from "fs";
+
+const composition: VideoConfig = {
+  width: 100,
+  height: 100,
+  fps: 30,
+  defaultProps: {},
+  props: {},
+  defaultCodec: null,
+  id: "hi",
+  durationInFrames: 100,
+};
+const serveUrl = "http://localhost:8080";
+const inputProps = {};
+// ---cut---
+import { renderMedia, OnArtifact } from "@remotion/renderer";
+
+const onArtifact: OnArtifact = (artifact) => {
+  console.log(artifact.filename); // string
+  console.log(artifact.content); // string | Uint8Array
+  console.log(artifact.frame); // number, frame in the composition which emitted this
+
+  // Example action: Write the artifact to disk
+  fs.writeFileSync(artifact.filename, artifact.content);
+};
+
+await renderMedia({
+  composition,
+  serveUrl,
+  onArtifact,
+  codec: "h264",
+  inputProps,
+});
+```
+
+### Using the Remotion Lambda CLI
+
+When using [`npx remotion lambda render`](/docs/lambda/cli/render) or [`npx remotion lambda still`](/docs/lambda/cli/still), artifacts get saved to the S3 bucket under the key `renders/[render-id]/artifacts/[filename]`.
+
+They will get logged to the console and you can click them to download them.  
+The `--privacy` option also applies to artifacts.
+
+### Using `renderMediaOnLambda()`
+
+When using [`renderMediaOnLambda()`](/docs/lambda/rendermediaonlambda), artifacts get saved to the S3 bucket under the key `renders/[render-id]/artifacts/[filename]`.
+
+You can obtain a list of currently received assets from [`getRenderProgress()`](/docs/lambda/getrenderprogress#artifacts).
+
+```tsx twoslash title="progress.ts"
+// @module: es2022
+// @target: es2017
+import { getRenderProgress } from "@remotion/lambda/client";
+
+const renderProgress = await getRenderProgress({
+  renderId: "hi",
+  functionName: "hi",
+  bucketName: "hi",
+  region: "eu-central-1",
+});
+
+for (const artifact of renderProgress.artifacts) {
+  console.log(artifact.filename); // "hello-world.txt"
+  console.log(artifact.sizeInBytes); // 12
+  console.log(artifact.s3Url); // "https://s3.eu-central-1.amazonaws.com/remotion-lambda-abcdef/renders/abcdef/artifacts/hello-world.txt"
+  console.log(artifact.s3Key); // "renders/abcdef/artifacts/hello-world.txt"
+}
+```
+
+### Using `renderStillOnLambda()`
+
+When using [`renderStillOnLambda()`](/docs/lambda/renderstillonlambda), artifacts get saved to the S3 bucket under the key `renders/[render-id]/artifacts/[filename]`.
+
+You can obtain a list of received assets from [`artifacts`](/docs/lambda/renderstillonlambda#artifacts) field of `renderStillOnLambda()`.
+
+```tsx twoslash title="still.ts"
+const serveUrl = "http://localhost:8080";
+const inputProps = {};
+const functionName = "hi";
+const composition = "hi";
+const privacy = "public" as const;
+const imageFormat = "png" as const;
+const region = "eu-central-1" as const;
+
+// ---cut---
+// @module: es2022
+// @target: es2017
+import { renderStillOnLambda } from "@remotion/lambda/client";
+
+const stillResponse = await renderStillOnLambda({
+  functionName,
+  region,
+  serveUrl,
+  composition,
+  inputProps,
+  imageFormat,
+  privacy,
+});
+
+for (const artifact of stillResponse.artifacts) {
+  console.log(artifact.filename); // "hello-world.txt"
+  console.log(artifact.sizeInBytes); // 12
+  console.log(artifact.s3Url); // "https://s3.eu-central-1.amazonaws.com/remotion-lambda-abcdef/renders/abcdef/artifacts/hello-world.txt"
+  console.log(artifact.s3Key); // "renders/abcdef/artifacts/hello-world.txt"
+}
+```
+
+### Using Cloud Run
+
+In the Cloud Run Alpha, emitting artifacts is not supported and will throw an error.  
+We plan on revising Cloud Run to use the same runtime as Lambda in the future and will bring this feature along.
+
+## See also
+
+- [`<Artifact>`](/docs/artifact)
diff --git a/packages/docs/docs/lambda/getrenderprogress.mdx b/packages/docs/docs/lambda/getrenderprogress.mdx
index 4772f48f67b..49561b65116 100644
--- a/packages/docs/docs/lambda/getrenderprogress.mdx
+++ b/packages/docs/docs/lambda/getrenderprogress.mdx
@@ -164,6 +164,10 @@ If the render is in progress, this is `null`. If the render is done, it is an ar
 - `timeInMilliseconds`: The time it took the render that chunk
 - `frameRange`: A tuple containing the first and last frame that was rendered in that chunk.
 
+### `artifacts`<AvailableFrom v="4.0.176"/>
+
+Artifacts that were created so far during the render. [See here for an example of dealing with field.](/docs/artifacts#using-rendermediaonlambda)
+
 ## See also
 
 - [Source code for this function](https://github.com/remotion-dev/remotion/blob/main/packages/lambda/src/api/get-render-progress.ts)
diff --git a/packages/docs/docs/lambda/renderstillonlambda.mdx b/packages/docs/docs/lambda/renderstillonlambda.mdx
index 2f0db3ec37a..ff0f3ae277c 100644
--- a/packages/docs/docs/lambda/renderstillonlambda.mdx
+++ b/packages/docs/docs/lambda/renderstillonlambda.mdx
@@ -292,6 +292,10 @@ _Available from v3.2.10_
 
 A link to CloudWatch (if you haven't disabled it) that you can visit to see the logs for the render.
 
+### `artifacts`<AvailableFrom v="4.0.176"/>
+
+Artifacts that were created so far during the render. [See here for an example of dealing with field.](/docs/artifacts#using-renderstillonlambda)
+
 ## See also
 
 - [Source code for this function](https://github.com/remotion-dev/remotion/blob/main/packages/lambda/src/api/render-still-on-lambda.ts)
diff --git a/packages/docs/docs/miscellaneous/linux-dependencies.mdx b/packages/docs/docs/miscellaneous/linux-dependencies.mdx
index 16721224e49..91dd7421e7e 100644
--- a/packages/docs/docs/miscellaneous/linux-dependencies.mdx
+++ b/packages/docs/docs/miscellaneous/linux-dependencies.mdx
@@ -1,4 +1,5 @@
 ---
+image: /generated/articles-docs-miscellaneous-linux-dependencies.png
 sidebar_label: Linux Dependencies
 title: Linux Dependencies
 crumb: "FAQ"
diff --git a/packages/docs/docs/renderer/render-frames.mdx b/packages/docs/docs/renderer/render-frames.mdx
index e365c82fc3a..c00b4e1ae6f 100644
--- a/packages/docs/docs/renderer/render-frames.mdx
+++ b/packages/docs/docs/renderer/render-frames.mdx
@@ -129,6 +129,10 @@ Disables audio output. This option may only be set in combination with a video c
 
 <Options id="log" />
 
+### `onArtifact?`<AvailableFrom v="4.0.176" />
+
+[Handle an artifact](/docs/artifacts#using-rendermedia-renderstill-or-renderframes) that was emitted by the [`<Artifact>`](/docs/artifact) component.
+
 ### `puppeteerInstance?`
 
 _optional_
diff --git a/packages/docs/docs/renderer/render-media.mdx b/packages/docs/docs/renderer/render-media.mdx
index 4e97b32fa10..8c9d73f2d6c 100644
--- a/packages/docs/docs/renderer/render-media.mdx
+++ b/packages/docs/docs/renderer/render-media.mdx
@@ -102,6 +102,10 @@ A `number` specifying how many render processes should be started in parallel, a
 
 <Options id="log" />
 
+### `onArtifact?`<AvailableFrom v="4.0.176" />
+
+[Handle an artifact](/docs/artifacts#using-rendermedia-renderstill-or-renderframes) that was emitted by the [`<Artifact>`](/docs/artifact) component.
+
 ### `audioCodec?`
 
 _"pcm-16" | "aac" | "mp3" | "opus", available from v3.3.41_
diff --git a/packages/docs/docs/renderer/render-still.mdx b/packages/docs/docs/renderer/render-still.mdx
index 6ebd37f7a82..13cfbfb1e84 100644
--- a/packages/docs/docs/renderer/render-still.mdx
+++ b/packages/docs/docs/renderer/render-still.mdx
@@ -127,6 +127,10 @@ An object containing key-value pairs of environment variables which will be inje
 
 <Options id="log" />
 
+### `onArtifact?`<AvailableFrom v="4.0.176" />
+
+[Handle an artifact](/docs/artifacts#using-rendermedia-renderstill-or-renderframes) that was emitted by the [`<Artifact>`](/docs/artifact) component.
+
 ### `overwrite?`
 
 _optional - default `true`_
diff --git a/packages/docs/sidebars.js b/packages/docs/sidebars.js
index 9c02a399951..15191594487 100644
--- a/packages/docs/sidebars.js
+++ b/packages/docs/sidebars.js
@@ -84,6 +84,7 @@ module.exports = {
       items: [
         "absolute-fill",
         "audio",
+        "artifact",
         "calculate-metadata",
         "cancel-render",
         "composition",
@@ -629,6 +630,7 @@ module.exports = {
         "render-all",
         "miscellaneous/video-formats",
         "video-vs-offthreadvideo",
+        "artifacts",
       ],
     },
     {
diff --git a/packages/docs/src/data/articles.ts b/packages/docs/src/data/articles.ts
index ceb3701af32..bbfc3875903 100644
--- a/packages/docs/src/data/articles.ts
+++ b/packages/docs/src/data/articles.ts
@@ -97,6 +97,20 @@ export const articles = [
     compId: "articles-docs-api",
     crumb: null,
   },
+  {
+    id: "artifact",
+    title: "<Artifact>",
+    relativePath: "docs/artifact.mdx",
+    compId: "articles-docs-artifact",
+    crumb: "API",
+  },
+  {
+    id: "artifacts",
+    title: "Emitting Artifacts",
+    relativePath: "docs/artifacts.mdx",
+    compId: "articles-docs-artifacts",
+    crumb: "Techniques",
+  },
   {
     id: "get-help",
     title: "Get help",
@@ -1721,6 +1735,13 @@ export const articles = [
     compId: "articles-docs-miscellaneous-embed-remotion-studio",
     crumb: "FAQ",
   },
+  {
+    id: "miscellaneous/linux-dependencies",
+    title: "Linux Dependencies",
+    relativePath: "docs/miscellaneous/linux-dependencies.mdx",
+    compId: "articles-docs-miscellaneous-linux-dependencies",
+    crumb: "FAQ",
+  },
   {
     id: "miscellaneous/linux-single-process",
     title: "Multiple cores on Linux",
diff --git a/packages/docs/static/generated/articles-docs-artifact.png b/packages/docs/static/generated/articles-docs-artifact.png
new file mode 100644
index 00000000000..252834e9a8e
Binary files /dev/null and b/packages/docs/static/generated/articles-docs-artifact.png differ
diff --git a/packages/docs/static/generated/articles-docs-artifacts.png b/packages/docs/static/generated/articles-docs-artifacts.png
new file mode 100644
index 00000000000..2ddc01c2728
Binary files /dev/null and b/packages/docs/static/generated/articles-docs-artifacts.png differ
diff --git a/packages/docs/static/generated/articles-docs-miscellaneous-linux-dependencies.png b/packages/docs/static/generated/articles-docs-miscellaneous-linux-dependencies.png
new file mode 100644
index 00000000000..3fc841acfe3
Binary files /dev/null and b/packages/docs/static/generated/articles-docs-miscellaneous-linux-dependencies.png differ
diff --git a/packages/example/src/Root.tsx b/packages/example/src/Root.tsx
index 20e943ef7f0..ee5ed32611f 100644
--- a/packages/example/src/Root.tsx
+++ b/packages/example/src/Root.tsx
@@ -4,9 +4,9 @@ import {
 	CalculateMetadataFunction,
 	Composition,
 	Folder,
-	Still,
 	getInputProps,
 	staticFile,
+	Still,
 } from 'remotion';
 import {z} from 'zod';
 import {TwentyTwoKHzAudio} from './22KhzAudio';
@@ -32,8 +32,8 @@ import {Layers} from './Layers';
 import {ManyAudio} from './ManyAudio';
 import {MissingImg} from './MissingImg';
 import {
-	OffthreadRemoteVideo,
 	calculateMetadataFn,
+	OffthreadRemoteVideo,
 } from './OffthreadRemoteVideo/OffthreadRemoteVideo';
 import {OrbScene} from './Orb';
 import {ShapesMorph} from './Paths/ShapesMorph';
@@ -47,8 +47,8 @@ import RiveVehicle from './Rive/RiveExample';
 import {ScalePath} from './ScalePath';
 import {
 	ArrayTest,
-	SchemaTest,
 	schemaArrayTestSchema,
+	SchemaTest,
 	schemaTestSchema,
 } from './SchemaTest';
 import {Scripts} from './Scripts';
@@ -74,6 +74,7 @@ import {
 } from './StudioApis/SaveDefaultProps';
 import {TriggerCalculateMetadata} from './StudioApis/TriggerCalculateMetadata';
 import {WriteStaticFile} from './StudioApis/WriteStaticFile';
+import './style.css';
 import {Tailwind} from './Tailwind';
 import {TenFrameTester} from './TenFrameTester';
 import {TextStroke} from './TextStroke';
@@ -90,7 +91,6 @@ import {VideoSpeed} from './VideoSpeed';
 import {VideoTesting} from './VideoTesting';
 import {WarpDemoOuter} from './WarpText';
 import {WarpDemo2} from './WarpText/demo2';
-import './style.css';
 import {WatchStaticDemo} from './watch-static';
 if (alias !== 'alias') {
 	throw new Error('should support TS aliases');
@@ -98,6 +98,7 @@ if (alias !== 'alias') {
 
 // @ts-expect-error no types
 import styles from './styles.module.scss';
+import {SubtitleArtifact} from './SubtitleArtifact/SubtitleArtifact';
 
 if (!styles.hithere) {
 	throw new Error('should support SCSS modules');
@@ -1309,6 +1310,16 @@ export const Index: React.FC = () => {
 					defaultProps={{color: 'green'}}
 				/>
 			</Folder>
+			<Folder name="Artifacts">
+				<Composition
+					id="subtitle"
+					component={SubtitleArtifact}
+					fps={30}
+					height={1000}
+					width={1000}
+					durationInFrames={10}
+				/>
+			</Folder>
 		</>
 	);
 };
diff --git a/packages/example/src/SubtitleArtifact/SubtitleArtifact.tsx b/packages/example/src/SubtitleArtifact/SubtitleArtifact.tsx
new file mode 100644
index 00000000000..fde60d1154b
--- /dev/null
+++ b/packages/example/src/SubtitleArtifact/SubtitleArtifact.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import {Artifact, useCurrentFrame} from 'remotion';
+
+export const SubtitleArtifact: React.FC = () => {
+	const frame = useCurrentFrame();
+
+	return frame === 0 ? (
+		<Artifact
+			content={
+				new Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33])
+			}
+			filename="hello-uint8.txt"
+		/>
+	) : frame === 1 ? (
+		<Artifact content="Hello World" filename="hello.txt" />
+	) : null;
+};
diff --git a/packages/lambda/src/api/render-still-on-lambda.ts b/packages/lambda/src/api/render-still-on-lambda.ts
index 6b9c6c297ce..fe00d4cfd51 100644
--- a/packages/lambda/src/api/render-still-on-lambda.ts
+++ b/packages/lambda/src/api/render-still-on-lambda.ts
@@ -5,6 +5,7 @@ import type {
 } from '@remotion/renderer';
 import type {BrowserSafeApis} from '@remotion/renderer/client';
 import {NoReactAPIs} from '@remotion/renderer/pure';
+import type {ReceivedArtifact} from '../functions/helpers/overall-render-progress';
 import type {RenderStillLambdaResponsePayload} from '../functions/still';
 import type {AwsRegion} from '../pricing/aws-regions';
 import {callLambdaWithStreaming} from '../shared/call-lambda';
@@ -67,6 +68,7 @@ export type RenderStillOnLambdaOutput = {
 	bucketName: string;
 	renderId: string;
 	cloudWatchLogs: string;
+	artifacts: ReceivedArtifact[];
 };
 
 const internalRenderStillOnLambda = async (
@@ -100,12 +102,16 @@ const internalRenderStillOnLambda = async (
 							});
 						}
 
+						if (message.type === 'error-occurred') {
+							reject(new Error(message.payload.error));
+						}
+
 						if (message.type === 'still-rendered') {
 							resolve(message.payload);
 						}
 					},
 					timeoutInTest: 120000,
-					retriesRemaining: 1,
+					retriesRemaining: input.maxRetries,
 				})
 					.then(() => {
 						reject(new Error('Expected response to be streamed'));
@@ -130,6 +136,7 @@ const internalRenderStillOnLambda = async (
 				renderId: res.renderId,
 				rendererFunctionName: null,
 			}),
+			artifacts: res.receivedArtifacts,
 		};
 	} catch (err) {
 		if ((err as Error).stack?.includes('UnrecognizedClientException')) {
diff --git a/packages/lambda/src/cli/commands/render/progress.ts b/packages/lambda/src/cli/commands/render/progress.ts
index 2ee2a50c77f..2f29c13b491 100644
--- a/packages/lambda/src/cli/commands/render/progress.ts
+++ b/packages/lambda/src/cli/commands/render/progress.ts
@@ -2,6 +2,7 @@ import {CliInternals} from '@remotion/cli';
 import {RenderInternals} from '@remotion/renderer';
 import {NoReactInternals} from 'remotion/no-react';
 import type {RenderProgress} from '../../../defaults';
+import type {ReceivedArtifact} from '../../../functions/helpers/overall-render-progress';
 import {truthy} from '../../../shared/truthy';
 
 type LambdaInvokeProgress = {
@@ -200,6 +201,31 @@ const makeTopRow = (overall: RenderProgress) => {
 	return CliInternals.chalk.gray(str);
 };
 
+export const makeArtifactProgress = (artifactProgress: ReceivedArtifact[]) => {
+	if (artifactProgress.length === 0) {
+		return null;
+	}
+
+	return artifactProgress
+		.map((artifact) => {
+			return [
+				CliInternals.chalk.blue('+ S3'.padEnd(CliInternals.LABEL_WIDTH)),
+				CliInternals.chalk.blue(
+					CliInternals.makeHyperlink({
+						url: artifact.s3Url,
+						fallback: artifact.filename,
+						text: artifact.s3Key,
+					}),
+				),
+				CliInternals.chalk.gray(
+					`${CliInternals.formatBytes(artifact.sizeInBytes)}`,
+				),
+			].join(' ');
+		})
+		.filter(truthy)
+		.join('\n');
+};
+
 export const makeProgressString = ({
 	downloadInfo,
 	overall,
@@ -214,6 +240,7 @@ export const makeProgressString = ({
 		...makeRenderProgress(overall),
 		makeCombinationProgress(overall),
 		downloadInfo ? makeDownloadProgress(downloadInfo) : null,
+		makeArtifactProgress(overall.artifacts),
 	]
 		.filter(NoReactInternals.truthy)
 		.join('\n');
diff --git a/packages/lambda/src/cli/commands/render/render.ts b/packages/lambda/src/cli/commands/render/render.ts
index 332cd2c4768..77a4be8be9a 100644
--- a/packages/lambda/src/cli/commands/render/render.ts
+++ b/packages/lambda/src/cli/commands/render/render.ts
@@ -3,6 +3,7 @@ import {ConfigInternals} from '@remotion/cli/config';
 import type {ChromiumOptions, LogLevel} from '@remotion/renderer';
 import {RenderInternals} from '@remotion/renderer';
 import {BrowserSafeApis} from '@remotion/renderer/client';
+import path from 'path';
 import {NoReactInternals} from 'remotion/no-react';
 import {downloadMedia} from '../../../api/download-media';
 import {getRenderProgress} from '../../../api/get-render-progress';
@@ -428,16 +429,11 @@ export const renderCommand = async (
 		);
 
 		if (newStatus.done) {
-			progressBar.update(
-				makeProgressString({
-					downloadInfo: null,
-					overall: newStatus,
-				}),
-				false,
-			);
+			let downloadOrNothing;
+
 			if (downloadName) {
 				const downloadStart = Date.now();
-				const {outputPath, sizeInBytes} = await downloadMedia({
+				const download = await downloadMedia({
 					bucketName: res.bucketName,
 					outPath: downloadName,
 					region: getAwsRegion(),
@@ -457,31 +453,58 @@ export const renderCommand = async (
 						);
 					},
 				});
+				downloadOrNothing = download;
 				progressBar.update(
 					makeProgressString({
 						downloadInfo: {
 							doneIn: Date.now() - downloadStart,
-							downloaded: sizeInBytes,
-							totalSize: sizeInBytes,
+							downloaded: download.sizeInBytes,
+							totalSize: download.sizeInBytes,
 						},
 						overall: newStatus,
 					}),
 					false,
 				);
-				Log.info({indent: false, logLevel});
-				Log.info({indent: false, logLevel});
+			}
+
+			Log.info({indent: false, logLevel});
+			Log.info(
+				{indent: false, logLevel},
+				CliInternals.chalk.blue('+ S3 '.padEnd(CliInternals.LABEL_WIDTH)),
+				CliInternals.chalk.blue(
+					CliInternals.makeHyperlink({
+						fallback: newStatus.outputFile as string,
+						text: newStatus.outKey as string,
+						url: newStatus.outputFile as string,
+					}),
+				),
+				CliInternals.chalk.gray(
+					CliInternals.formatBytes(newStatus.outputSizeInBytes as number),
+				),
+			);
+
+			if (downloadOrNothing) {
+				const relativeOutputPath = path.relative(
+					process.cwd(),
+					downloadOrNothing.outputPath,
+				);
 				Log.info(
 					{indent: false, logLevel},
-					'Done!',
-					outputPath,
-					CliInternals.formatBytes(sizeInBytes),
+					CliInternals.chalk.blue('↓'.padEnd(CliInternals.LABEL_WIDTH)),
+					CliInternals.chalk.blue(
+						CliInternals.makeHyperlink({
+							url: `file://${downloadOrNothing.outputPath}`,
+							text: relativeOutputPath,
+							fallback: downloadOrNothing.outputPath,
+						}),
+					),
+					CliInternals.chalk.gray(
+						CliInternals.formatBytes(downloadOrNothing.sizeInBytes),
+					),
 				);
-			} else {
-				Log.info({indent: false, logLevel});
-				Log.info({indent: false, logLevel});
-				Log.info({indent: false, logLevel}, 'Done! ' + newStatus.outputFile);
 			}
 
+			Log.info({indent: false, logLevel});
 			Log.info(
 				{indent: false, logLevel},
 				[
diff --git a/packages/lambda/src/cli/commands/still.ts b/packages/lambda/src/cli/commands/still.ts
index 328e99cad1e..61b2725a243 100644
--- a/packages/lambda/src/cli/commands/still.ts
+++ b/packages/lambda/src/cli/commands/still.ts
@@ -3,6 +3,7 @@ import {ConfigInternals} from '@remotion/cli/config';
 import type {ChromiumOptions, LogLevel} from '@remotion/renderer';
 import {RenderInternals} from '@remotion/renderer';
 import {BrowserSafeApis} from '@remotion/renderer/client';
+import path from 'path';
 import {NoReactInternals} from 'remotion/no-react';
 import {downloadMedia} from '../../api/download-media';
 import {renderStillOnLambda} from '../../api/render-still-on-lambda';
@@ -11,6 +12,7 @@ import {
 	DEFAULT_MAX_RETRIES,
 	DEFAULT_OUTPUT_PRIVACY,
 } from '../../shared/constants';
+import {getS3RenderUrl} from '../../shared/get-aws-urls';
 import {validatePrivacy} from '../../shared/validate-privacy';
 import {validateMaxRetries} from '../../shared/validate-retries';
 import {validateServeUrl} from '../../shared/validate-serveurl';
@@ -19,6 +21,7 @@ import {getAwsRegion} from '../get-aws-region';
 import {findFunctionName} from '../helpers/find-function-name';
 import {quit} from '../helpers/quit';
 import {Log} from '../log';
+import {makeArtifactProgress} from './render/progress';
 
 const {
 	offthreadVideoCacheSizeInBytesOption,
@@ -197,7 +200,14 @@ export const stillCommand = async (
 	Log.info(
 		{indent: false, logLevel},
 		CliInternals.chalk.gray(
-			`functionName = ${functionName}, imageFormat = ${imageFormat} (${imageFormatReason})`,
+			`Function: ${CliInternals.makeHyperlink({text: functionName, fallback: functionName, url: `https://${getAwsRegion()}.console.aws.amazon.com/lambda/home#/functions/${functionName}?tab=code`})}`,
+		),
+	);
+
+	Log.info(
+		{indent: false, logLevel},
+		CliInternals.chalk.gray(
+			`Image Format = ${imageFormat} (${imageFormatReason})`,
 		),
 	);
 
@@ -228,25 +238,61 @@ export const stillCommand = async (
 		scale,
 		forceHeight: height,
 		forceWidth: width,
-		onInit: ({cloudWatchLogs, renderId, lambdaInsightsUrl}) => {
-			Log.info(
-				{indent: false, logLevel},
-				chalk.gray(`Render invoked with ID = ${renderId}`),
-			);
+		onInit: ({cloudWatchLogs, lambdaInsightsUrl}) => {
 			Log.verbose(
 				{indent: false, logLevel},
-				`CloudWatch logs (if enabled): ${cloudWatchLogs}`,
+				`${CliInternals.makeHyperlink({
+					text: 'CloudWatch Logs',
+					url: cloudWatchLogs,
+					fallback: `CloudWatch Logs: ${cloudWatchLogs}`,
+				})} (if enabled)`,
 			);
 			Log.verbose(
 				{indent: false, logLevel},
-				`Lambda Insights (if enabled): ${lambdaInsightsUrl}`,
+				`${CliInternals.makeHyperlink({
+					text: 'Lambda Insights',
+					url: lambdaInsightsUrl,
+					fallback: `Lambda Insights: ${lambdaInsightsUrl}`,
+				})} (if enabled)`,
 			);
 		},
 		deleteAfter,
 	});
+	Log.info(
+		{indent: false, logLevel},
+		CliInternals.chalk.gray(
+			`Render ID: ${CliInternals.makeHyperlink({text: res.renderId, fallback: res.renderId, url: getS3RenderUrl({bucketName: res.bucketName, renderId: res.renderId, region: getAwsRegion()})})}`,
+		),
+	);
+	Log.info(
+		{indent: false, logLevel},
+		CliInternals.chalk.gray(
+			`Bucket: ${CliInternals.makeHyperlink({text: res.bucketName, fallback: res.bucketName, url: `https://${getAwsRegion()}.console.aws.amazon.com/s3/buckets/${res.bucketName}/?region=${getAwsRegion()}`})}`,
+		),
+	);
+
+	Log.info(
+		{
+			indent: false,
+			logLevel,
+		},
+		makeArtifactProgress(res.artifacts),
+	);
+
+	Log.info(
+		{indent: false, logLevel},
+		chalk.blue('+ S3'.padEnd(CliInternals.LABEL_WIDTH)),
+		chalk.blue(
+			CliInternals.makeHyperlink({
+				fallback: res.url,
+				url: res.url,
+				text: res.outKey,
+			}),
+		),
+		chalk.gray(formatBytes(res.sizeInBytes)),
+	);
 
 	if (downloadName) {
-		Log.info({indent: false, logLevel}, 'Finished rendering. Downloading...');
 		const {outputPath, sizeInBytes} = await downloadMedia({
 			bucketName: res.bucketName,
 			outPath: downloadName,
@@ -254,15 +300,18 @@ export const stillCommand = async (
 			renderId: res.renderId,
 			logLevel,
 		});
+		const relativePath = path.relative(process.cwd(), outputPath);
 		Log.info(
 			{indent: false, logLevel},
-			'Done!',
-			outputPath,
-			formatBytes(sizeInBytes),
+			chalk.blue('↓'.padEnd(CliInternals.LABEL_WIDTH)),
+			chalk.blue(
+				CliInternals.makeHyperlink({
+					url: 'file://' + outputPath,
+					text: relativePath,
+					fallback: outputPath,
+				}),
+			),
+			chalk.gray(formatBytes(sizeInBytes)),
 		);
-	} else {
-		Log.info({indent: false, logLevel}, `Finished still!`);
-		Log.info({indent: false, logLevel});
-		Log.info({indent: false, logLevel}, res.url);
 	}
 };
diff --git a/packages/lambda/src/functions/helpers/create-post-render-data.ts b/packages/lambda/src/functions/helpers/create-post-render-data.ts
index ba9ffd508d0..c2725428ccb 100644
--- a/packages/lambda/src/functions/helpers/create-post-render-data.ts
+++ b/packages/lambda/src/functions/helpers/create-post-render-data.ts
@@ -103,5 +103,6 @@ export const createPostRenderData = ({
 		deleteAfter: renderMetadata.deleteAfter,
 		estimatedBillingDurationInMilliseconds,
 		timeToCombine: timeToCombine ?? null,
+		artifactProgress: overallProgress.receivedArtifact,
 	};
 };
diff --git a/packages/lambda/src/functions/helpers/get-progress.ts b/packages/lambda/src/functions/helpers/get-progress.ts
index 0f3928027ce..e22af635f2d 100644
--- a/packages/lambda/src/functions/helpers/get-progress.ts
+++ b/packages/lambda/src/functions/helpers/get-progress.ts
@@ -110,6 +110,7 @@ export const getProgress = async ({
 			compositionValidated: overallProgress.compositionValidated,
 			functionLaunched: overallProgress.functionLaunched,
 			serveUrlOpened: overallProgress.serveUrlOpened,
+			artifacts: overallProgress.receivedArtifact,
 		};
 	}
 
@@ -257,5 +258,6 @@ export const getProgress = async ({
 		compositionValidated: overallProgress.compositionValidated,
 		functionLaunched: overallProgress.functionLaunched,
 		serveUrlOpened: overallProgress.serveUrlOpened,
+		artifacts: overallProgress.receivedArtifact,
 	};
 };
diff --git a/packages/lambda/src/functions/helpers/io.ts b/packages/lambda/src/functions/helpers/io.ts
index 09efc03e7a5..adc8d2f1909 100644
--- a/packages/lambda/src/functions/helpers/io.ts
+++ b/packages/lambda/src/functions/helpers/io.ts
@@ -99,7 +99,7 @@ export const lambdaDeleteFile = async ({
 type LambdaWriteFileInput = {
 	bucketName: string;
 	key: string;
-	body: ReadStream | string;
+	body: ReadStream | string | Uint8Array;
 	region: AwsRegion;
 	privacy: Privacy;
 	expectedBucketOwner: string | null;
diff --git a/packages/lambda/src/functions/helpers/overall-render-progress.ts b/packages/lambda/src/functions/helpers/overall-render-progress.ts
index ecc6557e89c..9d1f821331f 100644
--- a/packages/lambda/src/functions/helpers/overall-render-progress.ts
+++ b/packages/lambda/src/functions/helpers/overall-render-progress.ts
@@ -26,6 +26,14 @@ export type OverallRenderProgress = {
 	functionLaunched: number;
 	serveUrlOpened: number | null;
 	compositionValidated: number | null;
+	receivedArtifact: ReceivedArtifact[];
+};
+
+export type ReceivedArtifact = {
+	filename: string;
+	sizeInBytes: number;
+	s3Url: string;
+	s3Key: string;
 };
 
 export type OverallProgressHelper = {
@@ -55,6 +63,8 @@ export type OverallProgressHelper = {
 	get: () => OverallRenderProgress;
 	setServeUrlOpened: (timestamp: number) => void;
 	setCompositionValidated: (timestamp: number) => void;
+	addReceivedArtifact: (asset: ReceivedArtifact) => void;
+	getReceivedArtifacts: () => ReceivedArtifact[];
 };
 
 export const makeInitialOverallRenderProgress = (
@@ -78,6 +88,7 @@ export const makeInitialOverallRenderProgress = (
 		functionLaunched: Date.now(),
 		serveUrlOpened: null,
 		compositionValidated: null,
+		receivedArtifact: [],
 	};
 };
 
@@ -274,6 +285,13 @@ export const makeOverallRenderProgress = ({
 			renderProgress.retries.push(retry);
 			upload();
 		},
+		addReceivedArtifact(asset) {
+			renderProgress.receivedArtifact.push(asset);
+			upload();
+		},
+		getReceivedArtifacts() {
+			return renderProgress.receivedArtifact;
+		},
 		get: () => renderProgress,
 	};
 };
diff --git a/packages/lambda/src/functions/helpers/serialize-artifact.ts b/packages/lambda/src/functions/helpers/serialize-artifact.ts
new file mode 100644
index 00000000000..50e26c89983
--- /dev/null
+++ b/packages/lambda/src/functions/helpers/serialize-artifact.ts
@@ -0,0 +1,51 @@
+import type {EmittedArtifact} from '@remotion/renderer';
+
+export type SerializedArtifact = {
+	filename: string;
+	stringContent: string;
+	frame: number;
+	binary: boolean;
+};
+
+export const deserializeArtifact = (
+	serializedArtifact: SerializedArtifact,
+): EmittedArtifact => {
+	if (serializedArtifact.binary) {
+		const content = new TextEncoder().encode(
+			atob(serializedArtifact.stringContent),
+		);
+
+		return {
+			filename: serializedArtifact.filename,
+			content,
+			frame: serializedArtifact.frame,
+		};
+	}
+
+	return {
+		filename: serializedArtifact.filename,
+		content: serializedArtifact.stringContent,
+		frame: serializedArtifact.frame,
+	};
+};
+
+export const serializeArtifact = (
+	artifact: EmittedArtifact,
+): SerializedArtifact => {
+	if (artifact.content instanceof Uint8Array) {
+		const b64encoded = btoa(new TextDecoder('utf8').decode(artifact.content));
+		return {
+			filename: artifact.filename,
+			stringContent: b64encoded,
+			frame: artifact.frame,
+			binary: true,
+		};
+	}
+
+	return {
+		filename: artifact.filename,
+		stringContent: artifact.content,
+		frame: artifact.frame,
+		binary: false,
+	};
+};
diff --git a/packages/lambda/src/functions/helpers/stream-renderer.ts b/packages/lambda/src/functions/helpers/stream-renderer.ts
index 94fe98cada5..7f39b5ee604 100644
--- a/packages/lambda/src/functions/helpers/stream-renderer.ts
+++ b/packages/lambda/src/functions/helpers/stream-renderer.ts
@@ -1,4 +1,4 @@
-import type {LogLevel} from '@remotion/renderer';
+import type {EmittedArtifact, LogLevel} from '@remotion/renderer';
 import {RenderInternals} from '@remotion/renderer';
 import {writeFileSync} from 'fs';
 import {join} from 'path';
@@ -8,6 +8,7 @@ import {callLambdaWithStreaming} from '../../shared/call-lambda';
 import type {OnMessage} from '../streaming/streaming';
 import {getCurrentRegionInFunction} from './get-current-region';
 import type {OverallProgressHelper} from './overall-render-progress';
+import {deserializeArtifact} from './serialize-artifact';
 
 type StreamRendererResponse =
 	| {
@@ -26,6 +27,7 @@ const streamRenderer = ({
 	overallProgress,
 	files,
 	logLevel,
+	onArtifact,
 }: {
 	payload: LambdaPayload;
 	functionName: string;
@@ -33,6 +35,7 @@ const streamRenderer = ({
 	overallProgress: OverallProgressHelper;
 	files: string[];
 	logLevel: LogLevel;
+	onArtifact: (asset: EmittedArtifact) => {alreadyExisted: boolean};
 }) => {
 	if (payload.type !== LambdaRoutines.renderer) {
 		throw new Error('Expected renderer type');
@@ -97,6 +100,26 @@ const streamRenderer = ({
 				return;
 			}
 
+			if (message.type === 'artifact-emitted') {
+				const artifact = deserializeArtifact(message.payload.artifact);
+				RenderInternals.Log.info(
+					{indent: false, logLevel},
+					`Received artifact on frame ${message.payload.artifact.frame}:`,
+					artifact.filename,
+					artifact.content.length + 'bytes.',
+				);
+				const {alreadyExisted} = onArtifact(artifact);
+				if (alreadyExisted) {
+					return resolve({
+						type: 'error',
+						error: `Chunk ${payload.chunk} emitted an asset filename ${message.payload.artifact.filename} at frame ${message.payload.artifact.frame} but there is already another artifact with the same name. https://remotion.dev/docs/artifacts`,
+						shouldRetry: false,
+					});
+				}
+
+				return;
+			}
+
 			if (message.type === 'error-occurred') {
 				overallProgress.addErrorWithoutUpload(message.payload.errorInfo);
 				overallProgress.setFrames({
@@ -162,6 +185,7 @@ export const streamRendererFunctionWithRetry = async ({
 	outdir,
 	overallProgress,
 	logLevel,
+	onArtifact,
 }: {
 	payload: LambdaPayload;
 	functionName: string;
@@ -169,6 +193,7 @@ export const streamRendererFunctionWithRetry = async ({
 	overallProgress: OverallProgressHelper;
 	files: string[];
 	logLevel: LogLevel;
+	onArtifact: (asset: EmittedArtifact) => {alreadyExisted: boolean};
 }): Promise<unknown> => {
 	if (payload.type !== LambdaRoutines.renderer) {
 		throw new Error('Expected renderer type');
@@ -181,11 +206,12 @@ export const streamRendererFunctionWithRetry = async ({
 		overallProgress,
 		payload,
 		logLevel,
+		onArtifact,
 	});
 
 	if (result.type === 'error') {
 		if (!result.shouldRetry) {
-			throw result.error;
+			throw new Error(result.error);
 		}
 
 		overallProgress.addRetry({
@@ -205,6 +231,7 @@ export const streamRendererFunctionWithRetry = async ({
 				retriesLeft: payload.retriesLeft - 1,
 			},
 			logLevel,
+			onArtifact,
 		});
 	}
 };
diff --git a/packages/lambda/src/functions/helpers/write-lambda-error.ts b/packages/lambda/src/functions/helpers/write-lambda-error.ts
index 4f5d3a30ed5..db04989c89b 100644
--- a/packages/lambda/src/functions/helpers/write-lambda-error.ts
+++ b/packages/lambda/src/functions/helpers/write-lambda-error.ts
@@ -3,7 +3,7 @@ import {getFolderFiles} from './get-files-in-folder';
 import {errorIsOutOfSpaceError} from './is-enosp-err';
 
 export type LambdaErrorInfo = {
-	type: 'renderer' | 'browser' | 'stitcher' | 'webhook';
+	type: 'renderer' | 'browser' | 'stitcher' | 'webhook' | 'artifact';
 	message: string;
 	name: string;
 	stack: string;
diff --git a/packages/lambda/src/functions/index.ts b/packages/lambda/src/functions/index.ts
index 0923b3e130e..3fccf215c14 100644
--- a/packages/lambda/src/functions/index.ts
+++ b/packages/lambda/src/functions/index.ts
@@ -69,47 +69,50 @@ const innerHandler = async ({
 			params.logLevel,
 		);
 
-		await new Promise((resolve, reject) => {
-			const onStream = (payload: StreamingPayload) => {
-				const message = makeStreamPayload({
-					message: payload,
-				});
-				return new Promise<void>((innerResolve, innerReject) => {
-					responseWriter
-						.write(message)
-						.then(() => {
-							innerResolve();
-						})
-						.catch((err) => {
-							reject(err);
-							innerReject(err);
-						});
-				});
-			};
+		try {
+			await new Promise((resolve, reject) => {
+				const onStream = (payload: StreamingPayload) => {
+					const message = makeStreamPayload({
+						message: payload,
+					});
+					return new Promise<void>((innerResolve, innerReject) => {
+						responseWriter
+							.write(message)
+							.then(() => {
+								innerResolve();
+							})
+							.catch((err) => {
+								reject(err);
+								innerReject(err);
+							});
+					});
+				};
 
-			if (params.streamed) {
-				onStream({
-					type: 'render-id-determined',
-					payload: {renderId},
-				});
-			}
+				if (params.streamed) {
+					onStream({
+						type: 'render-id-determined',
+						payload: {renderId},
+					});
+				}
 
-			stillHandler({
-				expectedBucketOwner: currentUserId,
-				params,
-				renderId,
-				onStream,
-				timeoutInMilliseconds,
-			})
-				.then((r) => {
-					resolve(r);
+				stillHandler({
+					expectedBucketOwner: currentUserId,
+					params,
+					renderId,
+					onStream,
+					timeoutInMilliseconds,
 				})
-				.catch((err) => {
-					reject(err);
-				});
-		});
-
-		await responseWriter.end();
+					.then((r) => {
+						resolve(r);
+					})
+					.catch((err) => {
+						reject(err);
+					});
+			});
+			await responseWriter.end();
+		} catch (err) {
+			console.log({err});
+		}
 
 		return;
 	}
diff --git a/packages/lambda/src/functions/launch.ts b/packages/lambda/src/functions/launch.ts
index 78c8d2e58ae..62d73e407e9 100644
--- a/packages/lambda/src/functions/launch.ts
+++ b/packages/lambda/src/functions/launch.ts
@@ -1,10 +1,10 @@
 /* eslint-disable @typescript-eslint/no-use-before-define */
-import type {LogOptions} from '@remotion/renderer';
+import type {EmittedArtifact, LogOptions} from '@remotion/renderer';
 import {RenderInternals} from '@remotion/renderer';
 import {existsSync, mkdirSync, rmSync} from 'fs';
 import {join} from 'path';
 import {VERSION} from 'remotion/version';
-import type {EventEmitter} from 'stream';
+import {type EventEmitter} from 'stream';
 import {
 	compressInputProps,
 	decompressInputProps,
@@ -20,6 +20,7 @@ import {
 	CONCAT_FOLDER_TOKEN,
 	LambdaRoutines,
 	MAX_FUNCTIONS_PER_RENDER,
+	artifactName,
 } from '../shared/constants';
 import {DOCS_URL} from '../shared/docs-url';
 import {invokeWebhook} from '../shared/invoke-webhook';
@@ -41,6 +42,7 @@ import {
 	getBrowserInstance,
 } from './helpers/get-browser-instance';
 import {getCurrentRegionInFunction} from './helpers/get-current-region';
+import {lambdaWriteFile} from './helpers/io';
 import {mergeChunksAndFinishRender} from './helpers/merge-chunks';
 import type {OverallProgressHelper} from './helpers/overall-render-progress';
 import {makeOverallRenderProgress} from './helpers/overall-render-progress';
@@ -354,6 +356,69 @@ const innerLaunchHandler = async ({
 
 	const files: string[] = [];
 
+	const onArtifact = (artifact: EmittedArtifact): {alreadyExisted: boolean} => {
+		if (
+			overallProgress
+				.getReceivedArtifacts()
+				.find((a) => a.filename === artifact.filename)
+		) {
+			return {alreadyExisted: true};
+		}
+
+		const region = getCurrentRegionInFunction();
+		const s3Key = artifactName(renderMetadata.renderId, artifact.filename);
+
+		const start = Date.now();
+		RenderInternals.Log.info(
+			{indent: false, logLevel: params.logLevel},
+			'Writing artifact ' + artifact.filename + ' to S3',
+		);
+		lambdaWriteFile({
+			bucketName: renderBucketName,
+			key: s3Key,
+			body: artifact.content,
+			region,
+			privacy: params.privacy,
+			expectedBucketOwner: options.expectedBucketOwner,
+			downloadBehavior: params.downloadBehavior,
+			customCredentials,
+		})
+			.then(() => {
+				RenderInternals.Log.info(
+					{indent: false, logLevel: params.logLevel},
+					`Wrote artifact to S3 in ${Date.now() - start}ms`,
+				);
+				overallProgress.addReceivedArtifact({
+					filename: artifact.filename,
+					sizeInBytes: artifact.content.length,
+					s3Url: `https://s3.${region}.amazonaws.com/${renderBucketName}/${s3Key}`,
+					s3Key,
+				});
+			})
+			.catch((err) => {
+				overallProgress.addErrorWithoutUpload({
+					type: 'artifact',
+					message: (err as Error).message,
+					name: (err as Error).name as string,
+					stack: (err as Error).stack as string,
+					tmpDir: null,
+					frame: artifact.frame,
+					chunk: null,
+					isFatal: false,
+					attempt: 1,
+					willRetry: false,
+					totalAttempts: 1,
+				});
+				overallProgress.upload();
+				RenderInternals.Log.error(
+					{indent: false, logLevel: params.logLevel},
+					'Failed to write artifact to S3',
+					err,
+				);
+			});
+		return {alreadyExisted: false};
+	};
+
 	await Promise.all(
 		lambdaPayloads.map(async (payload) => {
 			await streamRendererFunctionWithRetry({
@@ -363,6 +428,7 @@ const innerLaunchHandler = async ({
 				overallProgress,
 				payload,
 				logLevel: params.logLevel,
+				onArtifact,
 			});
 		}),
 	);
diff --git a/packages/lambda/src/functions/renderer.ts b/packages/lambda/src/functions/renderer.ts
index 864fa7aaf76..f6ac91c2b0a 100644
--- a/packages/lambda/src/functions/renderer.ts
+++ b/packages/lambda/src/functions/renderer.ts
@@ -1,4 +1,9 @@
-import type {AudioCodec, BrowserLog, Codec} from '@remotion/renderer';
+import type {
+	AudioCodec,
+	BrowserLog,
+	Codec,
+	OnArtifact,
+} from '@remotion/renderer';
 import {RenderInternals} from '@remotion/renderer';
 import fs from 'node:fs';
 import path from 'node:path';
@@ -23,6 +28,7 @@ import {getCurrentRegionInFunction} from './helpers/get-current-region';
 import {startLeakDetection} from './helpers/leak-detection';
 import {onDownloadsHelper} from './helpers/on-downloads-logger';
 import type {RequestContext} from './helpers/request-context';
+import {serializeArtifact} from './helpers/serialize-artifact';
 import {timer} from './helpers/timer';
 import {getTmpDirStateIfENoSp} from './helpers/write-lambda-error';
 import type {OnStream} from './streaming/streaming';
@@ -167,6 +173,35 @@ const renderHandler = async ({
 		params.everyNthFrame,
 	);
 
+	const onArtifact: OnArtifact = (artifact) => {
+		RenderInternals.Log.info(
+			{indent: false, logLevel: params.logLevel},
+			`Received artifact on frame ${artifact.frame}:`,
+			artifact.filename,
+			artifact.content.length + 'bytes. Streaming to main function',
+		);
+		const startTimestamp = Date.now();
+		onStream({
+			type: 'artifact-emitted',
+			payload: {
+				artifact: serializeArtifact(artifact),
+			},
+		})
+			.then(() => {
+				RenderInternals.Log.info(
+					{indent: false, logLevel: params.logLevel},
+					`Streaming artifact ${artifact.filename} to main function took ${Date.now() - startTimestamp}ms`,
+				);
+			})
+			.catch((e) => {
+				RenderInternals.Log.error(
+					{indent: false, logLevel: params.logLevel},
+					`Error streaming artifact ${artifact.filename} to main function`,
+					e,
+				);
+			});
+	};
+
 	await new Promise<void>((resolve, reject) => {
 		RenderInternals.internalRenderMedia({
 			repro: false,
@@ -271,6 +306,7 @@ const renderHandler = async ({
 			onBrowserDownload: () => {
 				throw new Error('Should not download a browser in Lambda');
 			},
+			onArtifact,
 		})
 			.then(({slowestFrames}) => {
 				RenderInternals.Log.verbose(
diff --git a/packages/lambda/src/functions/still.ts b/packages/lambda/src/functions/still.ts
index 6df26d9a4a1..49c9d7aeb41 100644
--- a/packages/lambda/src/functions/still.ts
+++ b/packages/lambda/src/functions/still.ts
@@ -1,4 +1,4 @@
-import type {StillImageFormat} from '@remotion/renderer';
+import type {EmittedArtifact, StillImageFormat} from '@remotion/renderer';
 import {RenderInternals} from '@remotion/renderer';
 import fs from 'node:fs';
 import path from 'node:path';
@@ -6,18 +6,17 @@ import {NoReactInternals} from 'remotion/no-react';
 import {VERSION} from 'remotion/version';
 import {estimatePrice} from '../api/estimate-price';
 import {internalGetOrCreateBucket} from '../api/get-or-create-bucket';
-import {callLambda} from '../shared/call-lambda';
 import {cleanupSerializedInputProps} from '../shared/cleanup-serialized-input-props';
 import {decompressInputProps} from '../shared/compress-props';
 import type {
 	CostsInfo,
 	LambdaPayload,
-	LambdaPayloads,
 	RenderMetadata,
 } from '../shared/constants';
 import {
 	LambdaRoutines,
 	MAX_EPHEMERAL_STORAGE_IN_MB,
+	artifactName,
 	overallProgressKey,
 } from '../shared/constants';
 import {convertToServeUrl} from '../shared/convert-to-serve-url';
@@ -39,8 +38,10 @@ import {getCurrentRegionInFunction} from './helpers/get-current-region';
 import {getOutputUrlFromMetadata} from './helpers/get-output-url-from-metadata';
 import {lambdaWriteFile} from './helpers/io';
 import {onDownloadsHelper} from './helpers/on-downloads-logger';
+import type {ReceivedArtifact} from './helpers/overall-render-progress';
 import {makeInitialOverallRenderProgress} from './helpers/overall-render-progress';
 import {validateComposition} from './helpers/validate-composition';
+import {getTmpDirStateIfENoSp} from './helpers/write-lambda-error';
 import type {OnStream} from './streaming/streaming';
 
 type Options = {
@@ -194,6 +195,58 @@ const innerStillHandler = async ({
 		throw new Error('Should not download a browser in Lambda');
 	};
 
+	const receivedArtifact: ReceivedArtifact[] = [];
+
+	const {key, renderBucketName, customCredentials} = getExpectedOutName(
+		renderMetadata,
+		bucketName,
+		getCredentialsFromOutName(lambdaParams.outName),
+	);
+
+	const onArtifact = (artifact: EmittedArtifact): {alreadyExisted: boolean} => {
+		if (receivedArtifact.find((a) => a.filename === artifact.filename)) {
+			return {alreadyExisted: true};
+		}
+
+		const s3Key = artifactName(renderMetadata.renderId, artifact.filename);
+		receivedArtifact.push({
+			filename: artifact.filename,
+			sizeInBytes: artifact.content.length,
+			s3Url: `https://s3.${region}.amazonaws.com/${renderBucketName}/${s3Key}`,
+			s3Key,
+		});
+
+		const startTime = Date.now();
+		RenderInternals.Log.info(
+			{indent: false, logLevel: lambdaParams.logLevel},
+			'Writing artifact ' + artifact.filename + ' to S3',
+		);
+		lambdaWriteFile({
+			bucketName: renderBucketName,
+			key: s3Key,
+			body: artifact.content,
+			region,
+			privacy: lambdaParams.privacy,
+			expectedBucketOwner,
+			downloadBehavior: lambdaParams.downloadBehavior,
+			customCredentials,
+		})
+			.then(() => {
+				RenderInternals.Log.info(
+					{indent: false, logLevel: lambdaParams.logLevel},
+					`Wrote artifact to S3 in ${Date.now() - startTime}ms`,
+				);
+			})
+			.catch((err) => {
+				RenderInternals.Log.error(
+					{indent: false, logLevel: lambdaParams.logLevel},
+					'Failed to write artifact to S3',
+					err,
+				);
+			});
+		return {alreadyExisted: false};
+	};
+
 	await RenderInternals.internalRenderStill({
 		composition,
 		output: outputPath,
@@ -229,14 +282,9 @@ const innerStillHandler = async ({
 		offthreadVideoCacheSizeInBytes: lambdaParams.offthreadVideoCacheSizeInBytes,
 		binariesDirectory: null,
 		onBrowserDownload,
+		onArtifact,
 	});
 
-	const {key, renderBucketName, customCredentials} = getExpectedOutName(
-		renderMetadata,
-		bucketName,
-		getCredentialsFromOutName(lambdaParams.outName),
-	);
-
 	const {size} = await fs.promises.stat(outputPath);
 
 	await lambdaWriteFile({
@@ -284,6 +332,7 @@ const innerStillHandler = async ({
 		estimatedPrice: formatCostsInfo(estimatedPrice),
 		renderId,
 		outKey,
+		receivedArtifacts: receivedArtifact,
 	};
 
 	onStream({
@@ -301,13 +350,21 @@ export type RenderStillLambdaResponsePayload = {
 	sizeInBytes: number;
 	estimatedPrice: CostsInfo;
 	renderId: string;
+	receivedArtifacts: ReceivedArtifact[];
 };
 
 export const stillHandler = async (
 	options: Options,
-): Promise<{
-	type: 'success';
-}> => {
+): Promise<
+	| {
+			type: 'success';
+	  }
+	| {
+			type: 'error';
+			message: string;
+			stack: string;
+	  }
+> => {
 	const {params} = options;
 
 	if (params.type !== LambdaRoutines.still) {
@@ -323,10 +380,6 @@ export const stillHandler = async (
 		const isBrowserError = isFlakyError(err as Error);
 		const willRetry = isBrowserError || params.maxRetries > 0;
 
-		if (!willRetry) {
-			throw err;
-		}
-
 		RenderInternals.Log.error(
 			{
 				indent: false,
@@ -337,21 +390,34 @@ export const stillHandler = async (
 			'Will retry.',
 		);
 
-		const retryPayload: LambdaPayloads[LambdaRoutines.still] = {
-			...params,
-			maxRetries: params.maxRetries - 1,
-			attempt: params.attempt + 1,
-		};
-
-		const res = await callLambda({
-			functionName: process.env.AWS_LAMBDA_FUNCTION_NAME as string,
-			payload: retryPayload,
-			region: getCurrentRegionInFunction(),
-			type: LambdaRoutines.still,
-			timeoutInTest: 120000,
-		});
+		if (params.streamed) {
+			await options.onStream({
+				type: 'error-occurred',
+				payload: {
+					error: (err as Error).stack as string,
+					shouldRetry: willRetry,
+					errorInfo: {
+						name: (err as Error).name as string,
+						message: (err as Error).message as string,
+						stack: (err as Error).stack as string,
+						chunk: null,
+						frame: params.frame,
+						type: 'renderer',
+						isFatal: false,
+						tmpDir: getTmpDirStateIfENoSp((err as Error).stack as string),
+						attempt: params.attempt,
+						totalAttempts: 1 + params.maxRetries,
+						willRetry: true,
+					},
+				},
+			});
+		}
 
-		return res;
+		return {
+			type: 'error',
+			message: (err as Error).message,
+			stack: (err as Error).stack as string,
+		};
 	} finally {
 		forgetBrowserEventLoop(
 			options.params.type === LambdaRoutines.still
diff --git a/packages/lambda/src/functions/streaming/streaming.ts b/packages/lambda/src/functions/streaming/streaming.ts
index f7d8eea4e13..d9215838dec 100644
--- a/packages/lambda/src/functions/streaming/streaming.ts
+++ b/packages/lambda/src/functions/streaming/streaming.ts
@@ -1,4 +1,5 @@
 import {makeStreamPayloadMessage} from '@remotion/streaming';
+import type {SerializedArtifact} from '../helpers/serialize-artifact';
 import type {LambdaErrorInfo} from '../helpers/write-lambda-error';
 import type {RenderStillLambdaResponsePayload} from '../still';
 
@@ -10,6 +11,7 @@ const audioChunkRendered = 'audio-chunk-rendered' as const;
 const chunkComplete = 'chunk-complete' as const;
 const stillRendered = 'still-rendered' as const;
 const lambdaInvoked = 'lambda-invoked' as const;
+const artifactEmitted = 'artifact-emitted' as const;
 
 const messageTypes = {
 	'1': {type: framesRendered},
@@ -20,6 +22,7 @@ const messageTypes = {
 	'6': {type: stillRendered},
 	'7': {type: chunkComplete},
 	'8': {type: lambdaInvoked},
+	'9': {type: artifactEmitted},
 } as const;
 
 export type MessageTypeId = keyof typeof messageTypes;
@@ -34,6 +37,7 @@ export const formatMap: {[key in MessageType]: 'json' | 'binary'} = {
 	[stillRendered]: 'json',
 	[chunkComplete]: 'json',
 	[lambdaInvoked]: 'json',
+	[artifactEmitted]: 'json',
 };
 
 export type StreamingPayload =
@@ -82,6 +86,12 @@ export type StreamingPayload =
 			payload: {
 				attempt: number;
 			};
+	  }
+	| {
+			type: typeof artifactEmitted;
+			payload: {
+				artifact: SerializedArtifact;
+			};
 	  };
 
 export const messageTypeIdToMessageType = (
diff --git a/packages/lambda/src/shared/constants.ts b/packages/lambda/src/shared/constants.ts
index 8abd7bd221d..ec4ad1d54a4 100644
--- a/packages/lambda/src/shared/constants.ts
+++ b/packages/lambda/src/shared/constants.ts
@@ -14,6 +14,7 @@ import type {
 import type {BrowserSafeApis} from '@remotion/renderer/client';
 import type {ChunkRetry} from '../functions/helpers/get-retry-stats';
 import type {DeleteAfter} from '../functions/helpers/lifecycle';
+import type {ReceivedArtifact} from '../functions/helpers/overall-render-progress';
 import type {EnhancedErrorInfo} from '../functions/helpers/write-lambda-error';
 import type {AwsRegion} from '../pricing/aws-regions';
 import type {
@@ -85,6 +86,8 @@ export const outName = (renderId: string, extension: string) =>
 	`${rendersPrefix(renderId)}/out.${extension}`;
 export const outStillName = (renderId: string, imageFormat: StillImageFormat) =>
 	`${rendersPrefix(renderId)}/out.${imageFormat}`;
+export const artifactName = (renderId: string, name: string) =>
+	`${rendersPrefix(renderId)}/artifacts/${name}`;
 export const customOutName = (
 	renderId: string,
 	bucketName: string,
@@ -412,6 +415,7 @@ export type PostRenderData = {
 	estimatedBillingDurationInMilliseconds: number;
 	deleteAfter: DeleteAfter | null;
 	timeToCombine: number | null;
+	artifactProgress: ReceivedArtifact[];
 };
 
 export type CostsInfo = {
@@ -467,6 +471,7 @@ export type RenderProgress = {
 	functionLaunched: number;
 	serveUrlOpened: number | null;
 	compositionValidated: number | null;
+	artifacts: ReceivedArtifact[];
 };
 
 export type Privacy = 'public' | 'private' | 'no-acl';
diff --git a/packages/renderer/src/assets/calculate-asset-positions.ts b/packages/renderer/src/assets/calculate-asset-positions.ts
index b8f7fa1d3d4..ecadc15a266 100644
--- a/packages/renderer/src/assets/calculate-asset-positions.ts
+++ b/packages/renderer/src/assets/calculate-asset-positions.ts
@@ -1,4 +1,5 @@
-import type {TRenderAsset} from 'remotion/no-react';
+import type {AudioOrVideoAsset, TRenderAsset} from 'remotion/no-react';
+import {onlyAudioAndVideoAssets} from '../filter-asset-types';
 import {resolveAssetSrc} from '../resolve-asset-src';
 import {convertAssetToFlattenedVolume} from './flatten-volume-array';
 import type {Assets, MediaAsset, UnsafeAsset} from './types';
@@ -18,9 +19,13 @@ const findFrom = (target: TRenderAsset[], renderAsset: TRenderAsset) => {
 	return true;
 };
 
-const copyAndDeduplicateAssets = (renderAssets: TRenderAsset[]) => {
-	const deduplicated: TRenderAsset[] = [];
-	for (const renderAsset of renderAssets) {
+const copyAndDeduplicateAssets = (
+	renderAssets: TRenderAsset[],
+): AudioOrVideoAsset[] => {
+	const onlyAudioAndVideo = onlyAudioAndVideoAssets(renderAssets);
+	const deduplicated: AudioOrVideoAsset[] = [];
+
+	for (const renderAsset of onlyAudioAndVideo) {
 		if (!deduplicated.find((d) => d.id === renderAsset.id)) {
 			deduplicated.push(renderAsset);
 		}
@@ -29,7 +34,9 @@ const copyAndDeduplicateAssets = (renderAssets: TRenderAsset[]) => {
 	return deduplicated;
 };
 
-export const calculateAssetPositions = (frames: TRenderAsset[][]): Assets => {
+export const calculateAssetPositions = (
+	frames: AudioOrVideoAsset[][],
+): Assets => {
 	const assets: UnsafeAsset[] = [];
 
 	for (let frame = 0; frame < frames.length; frame++) {
diff --git a/packages/renderer/src/assets/convert-assets-to-file-urls.ts b/packages/renderer/src/assets/convert-assets-to-file-urls.ts
index 1d75a95bec1..ef3b6ad1b4a 100644
--- a/packages/renderer/src/assets/convert-assets-to-file-urls.ts
+++ b/packages/renderer/src/assets/convert-assets-to-file-urls.ts
@@ -1,4 +1,4 @@
-import type {TRenderAsset} from 'remotion/no-react';
+import type {AudioOrVideoAsset} from 'remotion/no-react';
 import type {LogLevel} from '../log-level';
 import type {FrameAndAssets} from '../render-frames';
 import type {RenderMediaOnDownload} from './download-and-map-assets-to-file';
@@ -25,13 +25,13 @@ export const convertAssetsToFileUrls = async ({
 	downloadMap: DownloadMap;
 	indent: boolean;
 	logLevel: LogLevel;
-}): Promise<TRenderAsset[][]> => {
+}): Promise<AudioOrVideoAsset[][]> => {
 	const chunks = chunk(assets, 1000);
-	const results: TRenderAsset[][][] = [];
+	const results: AudioOrVideoAsset[][][] = [];
 
 	for (const ch of chunks) {
 		const assetPromises = ch.map((frame) => {
-			const frameAssetPromises = frame.assets.map((a) => {
+			const frameAssetPromises = frame.audioAndVideoAssets.map((a) => {
 				return downloadAndMapAssetsToFileUrl({
 					renderAsset: a,
 					onDownload,
diff --git a/packages/renderer/src/assets/download-and-map-assets-to-file.ts b/packages/renderer/src/assets/download-and-map-assets-to-file.ts
index a34fa32217a..db8c3ebaa11 100644
--- a/packages/renderer/src/assets/download-and-map-assets-to-file.ts
+++ b/packages/renderer/src/assets/download-and-map-assets-to-file.ts
@@ -1,6 +1,6 @@
 import fs from 'node:fs';
 import path, {extname} from 'node:path';
-import type {TRenderAsset} from 'remotion/no-react';
+import type {AudioOrVideoAsset} from 'remotion/no-react';
 import {random} from 'remotion/no-react';
 import {isAssetCompressed} from '../compress-assets';
 import {ensureOutputDirectory} from '../ensure-output-directory';
@@ -350,12 +350,12 @@ export const downloadAndMapAssetsToFileUrl = async ({
 	logLevel,
 	indent,
 }: {
-	renderAsset: TRenderAsset;
+	renderAsset: AudioOrVideoAsset;
 	onDownload: RenderMediaOnDownload | null;
 	downloadMap: DownloadMap;
 	logLevel: LogLevel;
 	indent: boolean;
-}): Promise<TRenderAsset> => {
+}): Promise<AudioOrVideoAsset> => {
 	const cleanup = attachDownloadListenerToEmitter(downloadMap, onDownload);
 	const newSrc = await downloadAsset({
 		src: renderAsset.src,
diff --git a/packages/renderer/src/assets/types.ts b/packages/renderer/src/assets/types.ts
index 22624f43918..f22289f6aa6 100644
--- a/packages/renderer/src/assets/types.ts
+++ b/packages/renderer/src/assets/types.ts
@@ -1,10 +1,10 @@
-import type {TRenderAsset} from 'remotion/no-react';
+import type {AudioOrVideoAsset} from 'remotion/no-react';
 
 // An unsafe asset is an asset with looser types, which occurs
 // during construction of the asset list. Prefer the MediaAsset
 // type instead.
 export type UnsafeAsset = Omit<
-	TRenderAsset,
+	AudioOrVideoAsset,
 	'frame' | 'id' | 'volume' | 'mediaFrame' | 'audioStartFrom'
 > & {
 	startInVideo: number;
@@ -27,9 +27,9 @@ export type MediaAsset = Omit<UnsafeAsset, 'duration' | 'volume'> & {
 };
 
 export const uncompressMediaAsset = (
-	allRenderAssets: TRenderAsset[],
-	assetToUncompress: TRenderAsset,
-): TRenderAsset => {
+	allRenderAssets: AudioOrVideoAsset[],
+	assetToUncompress: AudioOrVideoAsset,
+): AudioOrVideoAsset => {
 	const isCompressed = assetToUncompress.src.match(/same-as-(.*)-([0-9]+)$/);
 	if (!isCompressed) {
 		return assetToUncompress;
diff --git a/packages/renderer/src/collect-assets.ts b/packages/renderer/src/collect-assets.ts
new file mode 100644
index 00000000000..279eba2f6ed
--- /dev/null
+++ b/packages/renderer/src/collect-assets.ts
@@ -0,0 +1,46 @@
+import type {ArtifactAsset, TRenderAsset} from 'remotion/no-react';
+import type {Page} from './browser/BrowserPage';
+import {puppeteerEvaluateWithCatch} from './puppeteer-evaluate';
+
+export const collectAssets = async ({
+	frame,
+	freePage,
+	timeoutInMilliseconds,
+}: {
+	frame: number;
+	freePage: Page;
+	timeoutInMilliseconds: number;
+}) => {
+	const {value} = await puppeteerEvaluateWithCatch<TRenderAsset[]>({
+		pageFunction: () => {
+			return window.remotion_collectAssets();
+		},
+		args: [],
+		frame,
+		page: freePage,
+		timeoutInMilliseconds,
+	});
+
+	const fixedArtifacts = value.map((asset) => {
+		if (asset.type !== 'artifact') {
+			return asset;
+		}
+
+		if (typeof asset.content !== 'string') {
+			throw new Error(
+				`Expected string content for artifact ${asset.id}, but got ${asset.content}`,
+			);
+		}
+
+		const stringOrUintArray = asset.binary
+			? new TextEncoder().encode(atob(asset.content as string))
+			: asset.content;
+
+		return {
+			...asset,
+			content: stringOrUintArray,
+		} as ArtifactAsset;
+	});
+
+	return fixedArtifacts;
+};
diff --git a/packages/renderer/src/compress-assets.ts b/packages/renderer/src/compress-assets.ts
index b0d6c28aaf0..d2ce0fe50d3 100644
--- a/packages/renderer/src/compress-assets.ts
+++ b/packages/renderer/src/compress-assets.ts
@@ -3,12 +3,12 @@
  * Since we track the `src` property for every frame, Node.JS can run out of memory easily. Instead of duplicating the src for every frame, we save memory by replacing the full base 64 encoded data with a string `same-as-[asset-id]-[frame]` referencing a previous asset with the same src.
  */
 
-import type {TRenderAsset} from 'remotion/no-react';
+import type {AudioOrVideoAsset} from 'remotion/no-react';
 
 export const compressAsset = (
-	previousRenderAssets: TRenderAsset[],
-	newRenderAsset: TRenderAsset,
-): TRenderAsset => {
+	previousRenderAssets: AudioOrVideoAsset[],
+	newRenderAsset: AudioOrVideoAsset,
+): AudioOrVideoAsset => {
 	if (newRenderAsset.src.length < 400) {
 		return newRenderAsset;
 	}
diff --git a/packages/renderer/src/filter-asset-types.ts b/packages/renderer/src/filter-asset-types.ts
new file mode 100644
index 00000000000..b7fe68b32e8
--- /dev/null
+++ b/packages/renderer/src/filter-asset-types.ts
@@ -0,0 +1,17 @@
+import type {
+	ArtifactAsset,
+	AudioOrVideoAsset,
+	TRenderAsset,
+} from 'remotion/no-react';
+
+export const onlyAudioAndVideoAssets = (
+	assets: TRenderAsset[],
+): AudioOrVideoAsset[] => {
+	return assets.filter(
+		(asset) => asset.type === 'audio' || asset.type === 'video',
+	) as AudioOrVideoAsset[];
+};
+
+export const onlyArtifact = (assets: TRenderAsset[]): ArtifactAsset[] => {
+	return assets.filter((asset) => asset.type === 'artifact') as ArtifactAsset[];
+};
diff --git a/packages/renderer/src/index.ts b/packages/renderer/src/index.ts
index d30ac4b6e7b..912d26d792c 100644
--- a/packages/renderer/src/index.ts
+++ b/packages/renderer/src/index.ts
@@ -108,7 +108,7 @@ export {X264Preset} from './options/x264-preset';
 export {PixelFormat} from './pixel-format';
 export {RemotionServer} from './prepare-server';
 export {ProResProfile} from './prores-profile';
-export {RenderFramesOptions, renderFrames} from './render-frames';
+export {OnArtifact, RenderFramesOptions, renderFrames} from './render-frames';
 export {
 	InternalRenderMediaOptions,
 	RenderMediaOnProgress,
@@ -122,6 +122,7 @@ export {
 	SelectCompositionOptions,
 	selectComposition,
 } from './select-composition';
+export {EmittedArtifact} from './serialize-artifact';
 export {
 	StitchFramesToVideoOptions,
 	stitchFramesToVideo,
diff --git a/packages/renderer/src/render-frames.ts b/packages/renderer/src/render-frames.ts
index b1885eee45b..384e6e0405a 100644
--- a/packages/renderer/src/render-frames.ts
+++ b/packages/renderer/src/render-frames.ts
@@ -2,8 +2,7 @@ import fs from 'node:fs';
 import path from 'node:path';
 import {performance} from 'perf_hooks';
 // eslint-disable-next-line no-restricted-imports
-import type {TAsset} from 'remotion';
-import type {VideoConfig} from 'remotion/no-react';
+import type {AudioOrVideoAsset, VideoConfig} from 'remotion/no-react';
 import {NoReactInternals} from 'remotion/no-react';
 import type {RenderMediaOnDownload} from './assets/download-and-map-assets-to-file';
 import {downloadAndMapAssetsToFileUrl} from './assets/download-and-map-assets-to-file';
@@ -23,6 +22,7 @@ import type {Compositor} from './compositor/compositor';
 import {compressAsset} from './compress-assets';
 import {cycleBrowserTabs} from './cycle-browser-tabs';
 import {handleJavascriptException} from './error-handling/handle-javascript-exception';
+import {onlyArtifact, onlyAudioAndVideoAssets} from './filter-asset-types';
 import {findRemotionRoot} from './find-closest-package-json';
 import type {FrameRange} from './frame-range';
 import {getActualConcurrency} from './get-concurrency';
@@ -53,6 +53,7 @@ import {puppeteerEvaluateWithCatch} from './puppeteer-evaluate';
 import type {BrowserReplacer} from './replace-browser';
 import {handleBrowserCrash} from './replace-browser';
 import {seekToFrame} from './seek-to-frame';
+import type {EmittedArtifact} from './serialize-artifact';
 import {setPropsAndEnv} from './set-props-and-env';
 import {takeFrameAndCompose} from './take-frame-and-compose';
 import {truthy} from './truthy';
@@ -67,7 +68,9 @@ import {wrapWithErrorHandling} from './wrap-with-error-handling';
 
 const MAX_RETRIES_PER_FRAME = 1;
 
-export type InternalRenderFramesOptions = {
+export type OnArtifact = (asset: EmittedArtifact) => void;
+
+type InternalRenderFramesOptions = {
 	onStart: null | ((data: OnStartData) => void);
 	onFrameUpdate:
 		| null
@@ -101,6 +104,7 @@ export type InternalRenderFramesOptions = {
 	serializedResolvedPropsWithCustomSchema: string;
 	parallelEncodingEnabled: boolean;
 	compositionStart: number;
+	onArtifact: OnArtifact | null;
 } & ToOptions<typeof optionsMap.renderFrames>;
 
 type InnerRenderFramesOptions = {
@@ -119,6 +123,7 @@ type InnerRenderFramesOptions = {
 	everyNthFrame: number;
 	onBrowserLog: null | ((log: BrowserLog) => void);
 	onFrameBuffer: null | ((buffer: Buffer, frame: number) => void);
+	onArtifact: OnArtifact | null;
 	onDownload: RenderMediaOnDownload | null;
 	timeoutInMilliseconds: number;
 	scale: number;
@@ -142,9 +147,15 @@ type InnerRenderFramesOptions = {
 	compositionStart: number;
 } & ToOptions<typeof optionsMap.renderFrames>;
 
+type ArtifactWithoutContent = {
+	frame: number;
+	filename: string;
+};
+
 export type FrameAndAssets = {
 	frame: number;
-	assets: TAsset[];
+	audioAndVideoAssets: AudioOrVideoAsset[];
+	artifactAssets: ArtifactWithoutContent[];
 };
 
 export type RenderFramesOptions = {
@@ -185,6 +196,7 @@ export type RenderFramesOptions = {
 	composition: VideoConfig;
 	muted?: boolean;
 	concurrency?: number | string | null;
+	onArtifact?: OnArtifact | null;
 	serveUrl: string;
 } & Partial<ToOptions<typeof optionsMap.renderFrames>>;
 
@@ -222,6 +234,7 @@ const innerRenderFrames = async ({
 	parallelEncodingEnabled,
 	compositionStart,
 	forSeamlessAacConcatenation,
+	onArtifact,
 }: Omit<
 	InnerRenderFramesOptions,
 	'offthreadVideoCacheSizeInBytes'
@@ -488,20 +501,49 @@ const innerRenderFrames = async ({
 
 		stopPerfMeasure(id);
 
-		const compressedAssets = collectedAssets.map((asset) =>
-			compressAsset(
-				assets
-					.filter(truthy)
-					.map((a) => a.assets)
-					.flat(2),
-				asset,
-			),
-		);
+		const previousAudioRenderAssets = assets
+			.filter(truthy)
+			.map((a) => a.audioAndVideoAssets)
+			.flat(2);
+
+		const previousArtifactAssets = assets
+			.filter(truthy)
+			.map((a) => a.artifactAssets)
+			.flat(2);
+
+		const audioAndVideoAssets = onlyAudioAndVideoAssets(collectedAssets);
+		const artifactAssets = onlyArtifact(collectedAssets);
+
+		for (const artifact of artifactAssets) {
+			for (const previousArtifact of previousArtifactAssets) {
+				if (artifact.filename === previousArtifact.filename) {
+					reject(
+						new Error(
+							`An artifact with output "${artifact.filename}" was already registered at frame ${previousArtifact.frame}, but now registered again at frame ${artifact.frame}. Artifacts must have unique names. https://remotion.dev/docs/artifacts`,
+						),
+					);
+					return;
+				}
+			}
+
+			onArtifact?.(artifact);
+		}
+
+		const compressedAssets = audioAndVideoAssets.map((asset) => {
+			return compressAsset(previousAudioRenderAssets, asset);
+		});
+
 		assets.push({
-			assets: compressedAssets,
+			audioAndVideoAssets: compressedAssets,
 			frame,
+			artifactAssets: artifactAssets.map((a) => {
+				return {
+					frame: a.frame,
+					filename: a.filename,
+				};
+			}),
 		});
-		compressedAssets.forEach((renderAsset) => {
+		for (const renderAsset of compressedAssets) {
 			downloadAndMapAssetsToFileUrl({
 				renderAsset,
 				onDownload,
@@ -518,7 +560,8 @@ const innerRenderFrames = async ({
 					),
 				);
 			});
-		});
+		}
+
 		if (!assetsOnly) {
 			framesRendered++;
 			onFrameUpdate?.(framesRendered, frame, performance.now() - startTime);
@@ -756,6 +799,7 @@ const internalRenderFramesRaw = ({
 	forSeamlessAacConcatenation,
 	compositionStart,
 	onBrowserDownload,
+	onArtifact,
 }: InternalRenderFramesOptions): Promise<RenderFramesOutput> => {
 	validateDimension(
 		composition.height,
@@ -885,6 +929,7 @@ const internalRenderFramesRaw = ({
 					forSeamlessAacConcatenation,
 					compositionStart,
 					onBrowserDownload,
+					onArtifact,
 				});
 			}),
 		])
@@ -979,6 +1024,7 @@ export const renderFrames = (
 		offthreadVideoCacheSizeInBytes,
 		binariesDirectory,
 		onBrowserDownload,
+		onArtifact,
 	} = options;
 
 	if (!composition) {
@@ -1050,5 +1096,6 @@ export const renderFrames = (
 		onBrowserDownload:
 			onBrowserDownload ??
 			defaultBrowserDownloadProgress({indent, logLevel, api: 'renderFrames()'}),
+		onArtifact: onArtifact ?? null,
 	});
 };
diff --git a/packages/renderer/src/render-media.ts b/packages/renderer/src/render-media.ts
index fce7039b5a7..191035cdc7f 100644
--- a/packages/renderer/src/render-media.ts
+++ b/packages/renderer/src/render-media.ts
@@ -53,6 +53,7 @@ import {prespawnFfmpeg} from './prespawn-ffmpeg';
 import {shouldUseParallelEncoding} from './prestitcher-memory-usage';
 import type {ProResProfile} from './prores-profile';
 import {validateSelectedCodecAndProResCombination} from './prores-profile';
+import type {OnArtifact} from './render-frames';
 import {internalRenderFrames} from './render-frames';
 import {
 	disableRepro,
@@ -125,6 +126,7 @@ export type InternalRenderMediaOptions = {
 	concurrency: number | string | null;
 	binariesDirectory: string | null;
 	compositionStart: number;
+	onArtifact: OnArtifact | null;
 } & MoreRenderMediaOptions;
 
 type Prettify<T> = {
@@ -179,6 +181,7 @@ export type RenderMediaOptions = Prettify<{
 	colorSpace?: ColorSpace;
 	repro?: boolean;
 	binariesDirectory?: string | null;
+	onArtifact?: OnArtifact;
 }> &
 	Partial<MoreRenderMediaOptions>;
 
@@ -241,6 +244,7 @@ const internalRenderMediaRaw = ({
 	forSeamlessAacConcatenation,
 	compositionStart,
 	onBrowserDownload,
+	onArtifact,
 }: InternalRenderMediaOptions): Promise<RenderMediaResult> => {
 	if (repro) {
 		enableRepro({
@@ -649,6 +653,7 @@ const internalRenderMediaRaw = ({
 					compositionStart,
 					forSeamlessAacConcatenation,
 					onBrowserDownload,
+					onArtifact,
 				});
 
 				return renderFramesProc;
@@ -863,6 +868,7 @@ export const renderMedia = ({
 	separateAudioTo,
 	forSeamlessAacConcatenation,
 	onBrowserDownload,
+	onArtifact,
 }: RenderMediaOptions): Promise<RenderMediaResult> => {
 	if (quality !== undefined) {
 		console.warn(
@@ -937,6 +943,7 @@ export const renderMedia = ({
 		onBrowserDownload:
 			onBrowserDownload ??
 			defaultBrowserDownloadProgress({indent, logLevel, api: 'renderMedia()'}),
+		onArtifact: onArtifact ?? null,
 		// TODO: In the future, introduce this as a public API when launching the distributed rendering API
 		compositionStart: 0,
 	});
diff --git a/packages/renderer/src/render-still.ts b/packages/renderer/src/render-still.ts
index 54b8453188d..c4fac575cd0 100644
--- a/packages/renderer/src/render-still.ts
+++ b/packages/renderer/src/render-still.ts
@@ -17,6 +17,7 @@ import type {Compositor} from './compositor/compositor';
 import {convertToPositiveFrameIndex} from './convert-to-positive-frame-index';
 import {ensureOutputDirectory} from './ensure-output-directory';
 import {handleJavascriptException} from './error-handling/handle-javascript-exception';
+import {onlyArtifact} from './filter-asset-types';
 import {findRemotionRoot} from './find-closest-package-json';
 import type {StillImageFormat} from './image-format';
 import {
@@ -34,6 +35,7 @@ import {DEFAULT_OVERWRITE} from './overwrite';
 import type {RemotionServer} from './prepare-server';
 import {makeOrReuseServer} from './prepare-server';
 import {puppeteerEvaluateWithCatch} from './puppeteer-evaluate';
+import type {OnArtifact} from './render-frames';
 import {seekToFrame} from './seek-to-frame';
 import {setPropsAndEnv} from './set-props-and-env';
 import {takeFrameAndCompose} from './take-frame-and-compose';
@@ -68,6 +70,7 @@ type InternalRenderStillOptions = {
 	serveUrl: string;
 	port: number | null;
 	offthreadVideoCacheSizeInBytes: number | null;
+	onArtifact: OnArtifact | null;
 } & ToOptions<typeof optionsMap.renderStill>;
 
 export type RenderStillOptions = {
@@ -99,6 +102,7 @@ export type RenderStillOptions = {
 	 * @deprecated Renamed to `jpegQuality`
 	 */
 	quality?: never;
+	onArtifact?: OnArtifact;
 } & Partial<ToOptions<typeof optionsMap.renderStill>>;
 
 type CleanupFn = () => Promise<unknown>;
@@ -130,6 +134,7 @@ const innerRenderStill = async ({
 	indent,
 	serializedResolvedPropsWithCustomSchema,
 	onBrowserDownload,
+	onArtifact,
 }: InternalRenderStillOptions & {
 	downloadMap: DownloadMap;
 	serveUrl: string;
@@ -316,7 +321,7 @@ const innerRenderStill = async ({
 		attempt: 0,
 	});
 
-	const {buffer} = await takeFrameAndCompose({
+	const {buffer, collectedAssets} = await takeFrameAndCompose({
 		frame: stillFrame,
 		freePage: page,
 		height: composition.height,
@@ -331,6 +336,23 @@ const innerRenderStill = async ({
 		timeoutInMilliseconds,
 	});
 
+	const artifactAssets = onlyArtifact(collectedAssets);
+	const previousArtifactAssets = [];
+
+	for (const artifact of artifactAssets) {
+		for (const previousArtifact of previousArtifactAssets) {
+			if (artifact.filename === previousArtifact.filename) {
+				throw new Error(
+					`An artifact with output "${artifact.filename}" was already registered at frame ${previousArtifact.frame}, but now registered again at frame ${artifact.frame}. Artifacts must have unique names. https://remotion.dev/docs/artifacts`,
+				);
+			}
+		}
+
+		previousArtifactAssets.push(artifact);
+
+		onArtifact?.(artifact);
+	}
+
 	await cleanup();
 
 	return {buffer: output ? null : buffer};
@@ -443,6 +465,7 @@ export const renderStill = (
 		logLevel: passedLogLevel,
 		binariesDirectory,
 		onBrowserDownload,
+		onArtifact,
 	} = options;
 
 	if (typeof jpegQuality !== 'undefined' && imageFormat !== 'jpeg') {
@@ -499,5 +522,6 @@ export const renderStill = (
 		onBrowserDownload:
 			onBrowserDownload ??
 			defaultBrowserDownloadProgress({indent, logLevel, api: 'renderStill()'}),
+		onArtifact: onArtifact ?? null,
 	});
 };
diff --git a/packages/renderer/src/seek-to-frame.ts b/packages/renderer/src/seek-to-frame.ts
index 1082ec7d0f3..f6d40ae8bca 100644
--- a/packages/renderer/src/seek-to-frame.ts
+++ b/packages/renderer/src/seek-to-frame.ts
@@ -113,7 +113,7 @@ export const waitForReady = ({
 						args: [],
 						frame,
 						page,
-						timeoutInMilliseconds,
+						timeoutInMilliseconds: 5000,
 					})
 						.then((res) => {
 							reject(
diff --git a/packages/renderer/src/serialize-artifact.ts b/packages/renderer/src/serialize-artifact.ts
new file mode 100644
index 00000000000..45b067da23b
--- /dev/null
+++ b/packages/renderer/src/serialize-artifact.ts
@@ -0,0 +1,5 @@
+export type EmittedArtifact = {
+	filename: string;
+	content: string | Uint8Array;
+	frame: number;
+};
diff --git a/packages/renderer/src/take-frame-and-compose.ts b/packages/renderer/src/take-frame-and-compose.ts
index 6324d63dfdb..8f47880a9e2 100644
--- a/packages/renderer/src/take-frame-and-compose.ts
+++ b/packages/renderer/src/take-frame-and-compose.ts
@@ -3,6 +3,7 @@ import path from 'node:path';
 import type {ClipRegion, TRenderAsset} from 'remotion/no-react';
 import type {DownloadMap} from './assets/download-map';
 import type {Page} from './browser/BrowserPage';
+import {collectAssets} from './collect-assets';
 import {compose} from './compositor/compose';
 import type {Compositor} from './compositor/compositor';
 import type {StillImageFormat, VideoImageFormat} from './image-format';
@@ -37,7 +38,7 @@ export const takeFrameAndCompose = async ({
 	compositor: Compositor;
 	timeoutInMilliseconds: number;
 }): Promise<{buffer: Buffer | null; collectedAssets: TRenderAsset[]}> => {
-	const [{value: clipRegion}, {value: collectedAssets}] = await Promise.all([
+	const [{value: clipRegion}, collectedAssets] = await Promise.all([
 		puppeteerEvaluateWithCatch<ClipRegion | null>({
 			pageFunction: () => {
 				if (typeof window.remotion_getClipRegion === 'undefined') {
@@ -51,15 +52,7 @@ export const takeFrameAndCompose = async ({
 			page: freePage,
 			timeoutInMilliseconds,
 		}),
-		puppeteerEvaluateWithCatch<TRenderAsset[]>({
-			pageFunction: () => {
-				return window.remotion_collectAssets();
-			},
-			args: [],
-			frame,
-			page: freePage,
-			timeoutInMilliseconds,
-		}),
+		collectAssets({frame, freePage, timeoutInMilliseconds}),
 	]);
 
 	if (imageFormat === 'none') {
diff --git a/packages/renderer/src/test/artifacts.test.tsx b/packages/renderer/src/test/artifacts.test.tsx
new file mode 100644
index 00000000000..879787c735c
--- /dev/null
+++ b/packages/renderer/src/test/artifacts.test.tsx
@@ -0,0 +1,88 @@
+/* eslint-disable no-restricted-imports */
+/**
+ * @vitest-environment jsdom
+ */
+import React from 'react';
+import {Artifact, useCurrentFrame} from 'remotion';
+import {expect, test} from 'vitest';
+import {getAssetsForMarkup} from './get-assets-for-markup';
+
+const basicConfig = {
+	width: 1080,
+	height: 1080,
+	fps: 30,
+	durationInFrames: 60,
+	id: 'hithere',
+};
+
+test('Should be able to collect artifacts', async () => {
+	const Markup: React.FC = () => {
+		const frame = useCurrentFrame();
+
+		return (
+			<div>
+				{frame === 1 ? (
+					<Artifact filename="hi.txt" content="hi there"></Artifact>
+				) : null}
+			</div>
+		);
+	};
+
+	const [first, second] = await getAssetsForMarkup(Markup, basicConfig);
+	expect(first).toEqual([]);
+	expect(second.length).toEqual(1);
+	const sec = second[0];
+	if (sec.type !== 'artifact') {
+		throw new Error('Expected artifact');
+	}
+
+	expect(sec.filename).toEqual('hi.txt');
+	expect(sec.content).toEqual('hi there');
+});
+
+test('Should throw on invalid artifact', async () => {
+	const Markup: React.FC = () => {
+		const frame = useCurrentFrame();
+
+		return (
+			<div>
+				{frame === 1 ? (
+					<Artifact
+						filename="backslash\\hithere.com"
+						content="hi there"
+					></Artifact>
+				) : null}
+			</div>
+		);
+	};
+
+	try {
+		await getAssetsForMarkup(Markup, basicConfig);
+	} catch (err) {
+		expect(err).toMatch(/The "filename" must match/);
+	}
+});
+
+test('Should throw on when missing content', async () => {
+	const Markup: React.FC = () => {
+		const frame = useCurrentFrame();
+
+		return (
+			<div>
+				{frame === 1 ? (
+					<Artifact
+						filename="backslash\\hithere.com"
+						// @ts-expect-error
+						content={undefined}
+					></Artifact>
+				) : null}
+			</div>
+		);
+	};
+
+	try {
+		await getAssetsForMarkup(Markup, basicConfig);
+	} catch (err) {
+		expect(err).toMatch(/The "content" must be a string/);
+	}
+});
diff --git a/packages/renderer/src/test/asset-calculation.test.tsx b/packages/renderer/src/test/asset-calculation.test.tsx
index d1bfc76a42d..1f40f9f33cb 100644
--- a/packages/renderer/src/test/asset-calculation.test.tsx
+++ b/packages/renderer/src/test/asset-calculation.test.tsx
@@ -7,6 +7,7 @@ import {Audio, interpolate, Sequence, useCurrentFrame, Video} from 'remotion';
 import {expect, test} from 'vitest';
 import {calculateAssetPositions} from '../assets/calculate-asset-positions';
 import type {MediaAsset} from '../assets/types';
+import {onlyAudioAndVideoAssets} from '../filter-asset-types';
 import {getAssetsForMarkup} from './get-assets-for-markup';
 
 const basicConfig = {
@@ -19,7 +20,11 @@ const basicConfig = {
 
 const getPositions = async (Markup: React.FC) => {
 	const assets = await getAssetsForMarkup(Markup, basicConfig);
-	return calculateAssetPositions(assets);
+	const onlyAudioAndVideo = assets.map((ass) => {
+		return onlyAudioAndVideoAssets(ass);
+	});
+
+	return calculateAssetPositions(onlyAudioAndVideo);
 };
 
 const withoutId = (asset: MediaAsset) => {
diff --git a/packages/renderer/src/test/asset-compression.test.ts b/packages/renderer/src/test/asset-compression.test.ts
index 36ee4d08fc6..0e22d40d577 100644
--- a/packages/renderer/src/test/asset-compression.test.ts
+++ b/packages/renderer/src/test/asset-compression.test.ts
@@ -2,6 +2,7 @@ import type {TRenderAsset} from 'remotion';
 import {expect, test} from 'vitest';
 import {calculateAssetPositions} from '../assets/calculate-asset-positions';
 import {compressAsset} from '../compress-assets';
+import {onlyAudioAndVideoAssets} from '../filter-asset-types';
 
 test('Should compress and uncompress assets', () => {
 	const uncompressed: TRenderAsset[] = [
@@ -35,8 +36,10 @@ test('Should compress and uncompress assets', () => {
 		],
 	].flat(1);
 
-	const compressedAssets = uncompressed.map((asset, i) => {
-		return compressAsset(uncompressed.slice(0, i), asset);
+	const onlyAudioAndVideo = onlyAudioAndVideoAssets(uncompressed);
+
+	const compressedAssets = onlyAudioAndVideo.map((asset, i) => {
+		return compressAsset(onlyAudioAndVideo.slice(0, i), asset);
 	});
 
 	expect(compressedAssets[0].src).toBe(String('x').repeat(1000));
diff --git a/packages/renderer/src/test/dont-skip-assets.test.ts b/packages/renderer/src/test/dont-skip-assets.test.ts
index 84ac0290bab..3f12d1cbae6 100644
--- a/packages/renderer/src/test/dont-skip-assets.test.ts
+++ b/packages/renderer/src/test/dont-skip-assets.test.ts
@@ -1,6 +1,7 @@
 import type {TRenderAsset} from 'remotion';
 import {expect, test} from 'vitest';
 import {calculateAssetPositions} from '../assets/calculate-asset-positions';
+import {onlyAudioAndVideoAssets} from '../filter-asset-types';
 
 type Truthy<T> = T extends false | '' | 0 | null | undefined ? never : T; // from lodash
 
@@ -9,7 +10,11 @@ function truthy<T>(value: T): value is Truthy<T> {
 }
 
 test('Dont skip assets', () => {
-	const assetPositions = calculateAssetPositions(mock);
+	const onlyAudioAndVideo = mock.map((m) => {
+		return onlyAudioAndVideoAssets(m);
+	});
+
+	const assetPositions = calculateAssetPositions(onlyAudioAndVideo);
 	expect(assetPositions).toEqual([
 		{
 			src: 'http://localhost:3000/4793bac32f610ffba8197b8a3422456f.mp3',
diff --git a/packages/renderer/src/test/get-assets-for-markup.tsx b/packages/renderer/src/test/get-assets-for-markup.tsx
index 93000ae7195..6b935fbecd5 100644
--- a/packages/renderer/src/test/get-assets-for-markup.tsx
+++ b/packages/renderer/src/test/get-assets-for-markup.tsx
@@ -52,6 +52,7 @@ export const getAssetsForMarkup = async (
 		const [renderAssets, setAssets] = useState<TRenderAsset[]>([]);
 
 		const registerRenderAsset = useCallback((renderAsset: TRenderAsset) => {
+			Internals.validateRenderAsset(renderAsset);
 			setAssets((assts) => {
 				return [...assts, renderAsset];
 			});
diff --git a/packages/studio-shared/src/index.ts b/packages/studio-shared/src/index.ts
index 11bf724e0d7..76d7d358be1 100644
--- a/packages/studio-shared/src/index.ts
+++ b/packages/studio-shared/src/index.ts
@@ -49,6 +49,7 @@ export {ProjectInfo} from './project-info';
 export type {RenderDefaults} from './render-defaults';
 export {
 	AggregateRenderProgress,
+	ArtifactProgress,
 	BundlingState,
 	CopyingState,
 	DownloadProgress,
diff --git a/packages/studio-shared/src/render-job.ts b/packages/studio-shared/src/render-job.ts
index 6ff3b0c1f8b..d22740d133a 100644
--- a/packages/studio-shared/src/render-job.ts
+++ b/packages/studio-shared/src/render-job.ts
@@ -56,6 +56,19 @@ export type AggregateRenderProgress = {
 	downloads: DownloadProgress[];
 	bundling: BundlingState;
 	copyingState: CopyingState;
+	artifactState: ArtifactProgress;
+};
+
+export type ReceivedArtifact = {
+	filename: string;
+	absoluteOutputDestination: string;
+	relativeOutputDestination: string;
+	sizeInBytes: number;
+	alreadyExisted: boolean;
+};
+
+export type ArtifactProgress = {
+	received: ReceivedArtifact[];
 };
 
 export type JobProgressCallback = (
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 55c9ec3684a..7309bb4ff75 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -5,7 +5,7 @@ settings:
   excludeLinksFromLockfile: false
 
 overrides:
-  caniuse-lite: 1.0.30001577
+  caniuse-lite: 1.0.30001636
 
 importers:
 
@@ -11570,7 +11570,7 @@ packages:
       postcss: ^8.1.0
     dependencies:
       browserslist: 4.21.5
-      caniuse-lite: 1.0.30001577
+      caniuse-lite: 1.0.30001636
       fraction.js: 4.2.0
       normalize-range: 0.1.2
       picocolors: 1.0.0
@@ -11586,7 +11586,7 @@ packages:
       postcss: ^8.1.0
     dependencies:
       browserslist: 4.21.5
-      caniuse-lite: 1.0.30001577
+      caniuse-lite: 1.0.30001636
       fraction.js: 4.2.0
       normalize-range: 0.1.2
       picocolors: 1.0.0
@@ -11925,7 +11925,7 @@ packages:
     engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
     hasBin: true
     dependencies:
-      caniuse-lite: 1.0.30001577
+      caniuse-lite: 1.0.30001636
       electron-to-chromium: 1.4.397
       escalade: 3.1.1
       node-releases: 1.1.77
@@ -11936,7 +11936,7 @@ packages:
     engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
     hasBin: true
     dependencies:
-      caniuse-lite: 1.0.30001577
+      caniuse-lite: 1.0.30001636
       electron-to-chromium: 1.4.397
       node-releases: 2.0.10
       update-browserslist-db: 1.0.11(browserslist@4.21.5)
@@ -11946,7 +11946,7 @@ packages:
     engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
     hasBin: true
     dependencies:
-      caniuse-lite: 1.0.30001577
+      caniuse-lite: 1.0.30001636
       electron-to-chromium: 1.4.640
       node-releases: 2.0.14
       update-browserslist-db: 1.0.13(browserslist@4.22.2)
@@ -12101,13 +12101,13 @@ packages:
     resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==}
     dependencies:
       browserslist: 4.21.5
-      caniuse-lite: 1.0.30001577
+      caniuse-lite: 1.0.30001636
       lodash.memoize: 4.1.2
       lodash.uniq: 4.5.0
     dev: false
 
-  /caniuse-lite@1.0.30001577:
-    resolution: {integrity: sha512-rs2ZygrG1PNXMfmncM0B5H1hndY5ZCC9b5TkFaVNfZ+AUlyqcMyVIQtc3fsezi0NUCk5XZfDf9WS6WxMxnfdrg==}
+  /caniuse-lite@1.0.30001636:
+    resolution: {integrity: sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==}
 
   /canvaskit-wasm@0.39.1:
     resolution: {integrity: sha512-Gy3lCmhUdKq+8bvDrs9t8+qf7RvcjuQn+we7vTVVyqgOVO1UVfHpsnBxkTZw+R4ApEJ3D5fKySl9TU11hmjl/A==}
@@ -14009,7 +14009,7 @@ packages:
       '@mdn/browser-compat-data': 5.5.19
       ast-metadata-inferer: 0.8.0
       browserslist: 4.22.2
-      caniuse-lite: 1.0.30001577
+      caniuse-lite: 1.0.30001636
       eslint: 8.56.0
       find-up: 5.0.0
       lodash.memoize: 4.1.2
@@ -17871,7 +17871,7 @@ packages:
       '@next/env': 14.1.4
       '@swc/helpers': 0.5.2
       busboy: 1.6.0
-      caniuse-lite: 1.0.30001577
+      caniuse-lite: 1.0.30001636
       graceful-fs: 4.2.11
       postcss: 8.4.31
       react: 18.3.1