File upload
Beta — Telefunc Stream is in beta: breaking changes may occur in any version update.
You can pass File and Blob arguments to a telefunction like any other value.
A single file, multiple files, or files mixed with other arguments — all are supported.
There are multiple streaming and reading strategies, see:
Example: basic
// FileUpload.telefunc.js
// Environment: server
import fs from 'node:fs'
export async function onUpload(file, description) {
// Stream to disk — constant memory, no matter the file size
const writable = fs.createWriteStream(`./uploads/${file.name}`)
for await (const chunk of file.stream()) {
writable.write(chunk)
}
writable.end()
console.log(`Saved ${file.name} (${file.size} bytes): ${description}`)
}// FileUpload.telefunc.ts
// Environment: server
import fs from 'node:fs'
export async function onUpload(file: File, description: string) {
// Stream to disk — constant memory, no matter the file size
const writable = fs.createWriteStream(`./uploads/${file.name}`)
for await (const chunk of file.stream()) {
writable.write(chunk)
}
writable.end()
console.log(`Saved ${file.name} (${file.size} bytes): ${description}`)
}// FileUpload.jsx
// Environment: client
import { onUpload } from './FileUpload.telefunc'
function FileUpload() {
return (
<form
onSubmit={async (e) => {
e.preventDefault()
const form = new FormData(e.currentTarget)
const file = form.get('file')
const description = form.get('description')
await onUpload(file, description)
}}
>
<input name="file" type="file" />
<input name="description" type="text" placeholder="Description" />
<button type="submit">Upload</button>
</form>
)
}// FileUpload.tsx
// Environment: client
import { onUpload } from './FileUpload.telefunc'
function FileUpload() {
return (
<form
onSubmit={async (e) => {
e.preventDefault()
const form = new FormData(e.currentTarget)
const file = form.get('file') as File
const description = form.get('description') as string
await onUpload(file, description)
}}
>
<input name="file" type="file" />
<input name="description" type="text" placeholder="Description" />
<button type="submit">Upload</button>
</form>
)
}Example: upload with progress
Combine file upload with a callback to report progress.
// Upload.telefunc.js
// Environment: server
import fs from 'node:fs'
export async function onUpload(file, onProgress) {
const total = file.size
let loaded = 0
const dest = fs.createWriteStream(`./uploads/${file.name}`)
for await (const chunk of file.stream()) {
dest.write(chunk)
loaded += chunk.byteLength
onProgress(Math.round((loaded / total) * 100))
}
dest.end()
}// Upload.telefunc.ts
// Environment: server
import fs from 'node:fs'
export async function onUpload(file: File, onProgress: (percent: number) => void) {
const total = file.size
let loaded = 0
const dest = fs.createWriteStream(`./uploads/${file.name}`)
for await (const chunk of file.stream()) {
dest.write(chunk)
loaded += chunk.byteLength
onProgress(Math.round((loaded / total) * 100))
}
dest.end()
}// Upload.jsx
// Environment: client
import { useState } from 'react'
import { onUpload } from './Upload.telefunc'
function Upload() {
const [progress, setProgress] = useState(0)
async function handleUpload(e) {
const file = e.target.files?.[0]
if (!file) return
await onUpload(file, (percent) => {
setProgress(percent)
})
}
return (
<div>
<input type="file" onChange={handleUpload} />
<div style={{ width: `${progress}%`, height: 4, background: '#3b82f6' }} />
</div>
)
}// Upload.tsx
// Environment: client
import { useState } from 'react'
import { onUpload } from './Upload.telefunc'
function Upload() {
const [progress, setProgress] = useState(0)
async function handleUpload(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0]
if (!file) return
await onUpload(file, (percent) => {
setProgress(percent)
})
}
return (
<div>
<input type="file" onChange={handleUpload} />
<div style={{ width: `${progress}%`, height: 4, background: '#3b82f6' }} />
</div>
)
}Streaming strategies
Environment: client.
Passing a File / Blob argument is one of several ways to move binary data with Telefunc:
| Method | Backpressure | Best for |
|---|---|---|
File / Blob argument | TCP-level | Finite uploads — files, images, CSV imports |
ReadableStream argument | Yes | Long-lived streams — video feed, sensor data, continuous audio |
new Channel().sendBinary() | Opt-in — await each send | Low-latency frames — skip the await for fire-and-forget |
new BroadcastChannel().publishBinary() | Publish-side only | Broadcast binary — video to multiple subscribers |
Reading strategies
Environment: server.
Each file argument is a standard File / Blob object:
| Method | Memory | Best for |
|---|---|---|
file.stream() | Constant (chunk-sized) | Pipe to disk, S3, or any writable stream |
file.arrayBuffer() | Full file in memory | Process entire file at once |
file.text() | Full file in memory | Read text content |
For large files, always use
file.stream()— memory stays constant regardless of file size.
Error handling
You handle errors the same way as you do for Telefunc streams: Guides > Stream > Error handling.
Limitations
File bytes flow directly from the HTTP stream to your code with zero internal buffering. This makes uploads memory-efficient — but there are two trade-offs:
One-shot reads
Each file can only be read once — calling .stream(), .text(), or .arrayBuffer() a second time throws.
If you need the data more than once, buffer it into a variable first.
Read in order
Multiple file arguments must be read in signature order:
// ✅ Correct order
await file1.text()
await file2.text()
// ❌ Out of order — file1 is discarded
await file2.text()
await file1.text()All files of a single call share one forward-only HTTP stream. Reading
file2beforefile1would require bufferingfile1in memory.
You can start reads concurrently — they stream in the correct order automatically:
// ✅ Works — both start in parallel, streamed in order
await Promise.all([
file1.stream().pipeTo(dest1),
file2.stream().pipeTo(dest2),
])How it works
You can skip this section — read it only if you're curious.
Telefunc uses a custom binary protocol — no multipart/form-data, no internal buffering.
- The client serializes the call into a binary request: metadata first, followed by raw file bytes.
- The server parses metadata and creates lazy
File/Blobobjects that reference the HTTP body stream without reading it. - When your telefunction calls
file.stream(), bytes are pulled directly from the HTTP stream on demand.
File bytes only flow through memory when you read them — and if you stream to disk, memory consumption is constant regardless of file size.
When a call contains files, Telefunc automatically switches from JSON to a binary format that streams file bytes without buffering.