Function Upload API
Upload a function as a ZIP or tar.gz archive; the API normalizes it to tar.gz and starts the cloud build.
Endpoint
POST /projects/{project_id}/functionsUpload a function as a ZIP or tar.gz source archive. The API accepts either format, injects build metadata, and stores a normalized tar.gz source archive before starting the cloud build.
Request Format
Content-Type: multipart/form-data
Form Fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Function name (unique within project) |
code | file (binary) | Yes | ZIP or tar.gz archive containing function source code plus dependency manifests/lockfiles |
runtime | string | Yes | Runtime environment |
handler | string | No | Function name to invoke (default: handler) |
Supported Runtimes
nodejs22.xnodejs24.xpython3.10python3.11python3.12python3.13python3.14ruby3.3ruby3.4ruby4.0
python3.10 is deprecated: Python 3.10 reached end of life in October 2026. It still deploys, and Volcano will remove it before 2027-08-31. Move functions to a newer Python runtime.
Examples
cURL
curl -X POST https://api.volcano.dev/projects/PROJECT_ID/functions \
-H "Authorization: Bearer $PLATFORM_TOKEN" \
-F "name=my-api" \
-F "code=@function.zip" \
-F "runtime=nodejs24.x" \
-F "handler=handler"JavaScript (fetch)
const formData = new FormData();
formData.append('name', 'my-api');
formData.append('code', fileInput.files[0]); // File from <input type="file">
formData.append('runtime', 'nodejs24.x');
formData.append('handler', 'handler');
const response = await fetch(`https://api.volcano.dev/projects/${projectId}/functions`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
body: formData,
});
const result = await response.json();
console.log('Function created:', result);JavaScript (Node.js with form-data)
const FormData = require('form-data');
const fs = require('fs');
const form = new FormData();
form.append('name', 'my-api');
form.append('code', fs.createReadStream('./function.zip'));
form.append('runtime', 'nodejs24.x');
form.append('handler', 'handler');
const response = await fetch(`https://api.volcano.dev/projects/${projectId}/functions`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
...form.getHeaders(),
},
body: form,
});Python (requests)
import requests
files = {
'name': (None, 'my-api'),
'code': ('function.tar.gz', open('function.tar.gz', 'rb'), 'application/gzip'),
'runtime': (None, 'python3.12'),
'handler': (None, 'lambda_handler')
}
response = requests.post(
f'https://api.volcano.dev/projects/{project_id}/functions',
headers={'Authorization': f'Bearer {token}'},
files=files
)
print(response.json())Go
package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
func uploadFunction(projectID, token string) error {
// Create multipart form
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// Add form fields
writer.WriteField("name", "my-api")
writer.WriteField("runtime", "nodejs24.x")
writer.WriteField("handler", "handler")
// Add file
file, _ := os.Open("function.zip")
defer file.Close()
part, _ := writer.CreateFormFile("code", "function.zip")
io.Copy(part, file)
writer.Close()
// Make request
url := fmt.Sprintf("https://api.volcano.dev/projects/%s/functions", projectID)
req, _ := http.NewRequest("POST", url, body)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", writer.FormDataContentType())
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}You may upload either ZIP or tar.gz source archives. The Volcano CLI always uploads tar.gz, but direct API clients can use either format.
Response
Success (201 Created)
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "123e4567-e89b-12d3-a456-426614174000",
"name": "my-api",
"status": "provisioning",
"runtime": "nodejs24.x",
"handler": "handler",
"created_at": "2024-01-05T10:30:00Z",
"updated_at": "2024-01-05T10:30:00Z"
}Error (400 Bad Request)
{
"error": "function code too large (max 256 MB, got 312475648 bytes from file 'function.zip')"
}Size Limits
Function source archives are limited to 256 MB. The API enforces this limit both before storage and after ZIP-to-tar.gz normalization. The CLI does not enforce it locally.
After upload, the cloud compile build installs dependencies from manifests such as package.json, requirements.txt, and Gemfile. The publish build then rejects a final function image larger than 4096 MB.
Recommendations:
| Package Size | Status |
|---|---|
| < 1MB | Excellent |
| 1-10MB | Good |
| 10-50MB | Large (consider optimization) |
| Above the configured upload limit | Rejected |
File Requirements
- Must be a ZIP or tar.gz file - Other formats rejected
- Include the handler's module - usually
index.js,index.mjsorindex.cjsfor Node.js,main.pyfor Python, ormain.rbfor Ruby, exporting the submittedhandlerfunction. The runtime can also load it from an installed dependency or a compiled module. If the runtime can't find it, the compile build fails, unless a project variable such asNODE_PATH,PYTHONPATHorRUBYLIBadds a directory it loads modules from. Upload compiled JavaScript: Volcano doesn't compile TypeScript. - Include dependency manifests/lockfiles - Do not include installed dependency directories for cloud deploys
- No symlink entries - Uploaded source archives cannot contain symlinks. Safe symlinks created during the cloud build, such as package-manager workspace links, are materialized before publish.
- No top-level
.volcano-dependenciespath - Volcano reserves it for dependency staging. An upload containing it is accepted asynchronously, then fails during the compile build. - Valid archive structure - The archive must be readable by Volcano
- No parent-directory entries - Archive paths such as
..,../, andsrc/../..are rejected.
Direct Upload Layout Inference
When you upload directly to the API, Volcano generates .volcano/function-build.json automatically. You do not need to create this file.
Volcano infers build roots deterministically:
function_rootis the directory containing the single runtime entry file when exactly one is found:index.js/index.mjsfor Node.js,main.pyfor Python, ormain.rbfor Ruby.- If there is no root-level file and all files are under one top-level directory, that top-level directory becomes
function_root. - Otherwise,
function_rootis the archive root (.). install_rootprefers the closest dependency manifest directory containing the runtime handler module, resolved relative tofunction_root.- When
function_rootis.and no manifest contains the handler module, the fallback selects the longest nested manifest directory path by character count; equal-length ties choose the lexicographically first directory. With no matching manifest or applicable fallback,install_rootisfunction_root.
Volcano prefixes the supplied handler with index. for Node.js or main.
for Python and Ruby. For Node.js and Ruby, the module runs up to the first dot
after the last /; the rest names the export, or Ruby's Class.method. For
Python, the final dot separates the module from the function. For example, the
Node.js handler src/handler.main becomes index.src/handler.main, so its
module file is index.src/handler.js, relative to function_root. Python
module dots become path separators; Node.js and Ruby preserve module directory
dots.
When a root-level dependency manifest exists, unrelated nested manifests do
not select the install root. For a root-level index.js and package.json,
Volcano installs from the archive root even when the
bundle also contains examples/other-package/package.json.
Recommended direct-upload layouts:
# Simple function
index.js
package.json
package-lock.json
# Nested function
apps/api/index.js
apps/api/package.json
# Monorepo function with root install
package.json
package-lock.json
apps/api/index.jsCommon Errors
"code file is required"
You forgot to include the code field:
# Bad - missing code
curl -F "name=my-func" ...
# Good - includes code
curl -F "name=my-func" -F "code=@function.zip" ..."code must be a valid .tar.gz or .zip archive"
The file you uploaded isn't a supported archive:
# Check file type
file function.zip
# Should output: "Zip archive data""function code too large"
Your source archive exceeds the 256 MB limit. Common solutions:
- Remove unnecessary files
- Bundle and minify application code
- Exclude installed dependencies, dependency caches, and development-only files
Function image too large
A deployment fails after its build when the built function, with its installed dependencies, is larger than 4096 MB. Remove unnecessary runtime files, reduce dependency size, or split the function into smaller functions.
Updating a Function
Use the same endpoint with the same function name:
# First deployment
curl -F "name=my-api" -F "code=@v1.zip" ...
# Update (same name, new code)
curl -F "name=my-api" -F "code=@v2.zip" ...Returns 200 OK for updates (not 201 Created).
Batch Deployments
POST /projects/{project_id}/functions/batch accepts multiple ZIP or tar.gz source archives in one multipart request. Each function still gets its own compile/publish workflow.
A single batch request can include up to 100 functions. Use multiple batch requests for larger projects.
If one function fails before its workflow starts, successful functions are left running. Failed new functions are deleted, and failed updates are rolled back to their previous metadata/status where possible. Partial failures are returned in the response failed array.
Benefits of Binary Upload
No base64 overhead - 33% more efficient
Faster uploads - Direct binary transfer
Simpler - Standard file upload
Works with standard tooling - Any client that can send multipart form data can upload functions
Browser compatible - Works with <input type="file">