Edit

File upload

BetaTelefunc 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:

MethodBackpressureBest for
File / Blob argumentTCP-levelFinite uploads — files, images, CSV imports
ReadableStream argumentYesLong-lived streams — video feed, sensor data, continuous audio
new Channel().sendBinary()Opt-inawait each sendLow-latency frames — skip the await for fire-and-forget
new BroadcastChannel().publishBinary()Publish-side onlyBroadcast binary — video to multiple subscribers

Reading strategies

Environment: server.

Each file argument is a standard File / Blob object:

MethodMemoryBest for
file.stream()Constant (chunk-sized)Pipe to disk, S3, or any writable stream
file.arrayBuffer()Full file in memoryProcess entire file at once
file.text()Full file in memoryRead 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 file2 before file1 would require buffering file1 in 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.

  1. The client serializes the call into a binary request: metadata first, followed by raw file bytes.
  2. The server parses metadata and creates lazy File/Blob objects that reference the HTTP body stream without reading it.
  3. 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.

See also